Package Camelot :: Package camelot :: Package view :: Module elixir_admin |
[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  import logging
     29  logger = logging.getLogger('camelot.view.elixir_admin')
     30

     31  import sqlalchemy.sql.expression
     32

     33  from camelot.admin.object_admin import ObjectAdmin
     34  from camelot.view.model_thread import post, model_function, gui_function
     35  from camelot.core.utils import ugettext_lazy
     36  from camelot.admin.validator.entity_validator import EntityValidator
     37

     38

     39 -class EntityAdmin(ObjectAdmin):
     40      """Admin class specific for classes that are mapped by sqlalchemy.

     41      This allows for much more introspection than the standard ObjectAdmin.

     42      """
     43

     44      validator = EntityValidator
     45

     46      @model_function
     47 -    def get_query(self):
     48          """:return: an sqlalchemy query for all the objects that should be

     49          displayed in the table or the selection view.  Overwrite this method to

     50          change the default query, which selects all rows in the database.

     51          """
     52          return self.entity.query
     53

     54      @model_function
     55 -    def get_subclass_entity_admin(self, entity):
     56          """Get the admin class for an entity that is a subclass of this admin's

     57          entity or this admin's entity itself.

     58          """
     59          for subclass_admin in self.get_subclasses():
     60              if subclass_admin.entity == entity:
     61                  return subclass_admin
     62          return self
     63

     64      @model_function
     65 -    def get_subclasses(self):
     66          """Returns admin objects for the subclasses of the Entity represented

     67          by this admin object.

     68          """
     69          if not self._subclasses:
     70              from elixir import entities
     71              self._subclasses = [

     72                  e.Admin(self.app_admin, e)

     73                  for e in entities

     74                      if (

     75                          issubclass(e, (self.entity,))

     76                          and hasattr(e, 'Admin')

     77                          and e != self.entity

     78                      )

     79              ]
     80          return self._subclasses
     81

     82      @model_function
     83 -    def get_verbose_identifier(self, obj):
     84          if hasattr(obj, 'id') and obj.id:
     85              if hasattr(obj, '__unicode__'):
     86                  return u'%s %s : %s' % (

     87                      unicode(self.get_verbose_name()),

     88                      unicode(obj.id),

     89                      unicode(obj)

     90                  )
     91              else:
     92                  return u'%s %s' % (

     93                      self.get_verbose_name(),

     94                      unicode(obj.id)

     95                  )
     96          else:
     97              return self.get_verbose_name()
     98

     99      @model_function
    100 -    def get_field_attributes(self, field_name):
    101          """Get the attributes needed to visualize the field field_name

    102          :param field_name: the name of the field

    103

    104          :return: a dictionary of attributes needed to visualize the field,

    105          those attributes can be:

    106           * python_type : the corresponding python type of the object

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

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

    109           * ...

    110          """
    111          try:
    112              return self._field_attributes[field_name]
    113          except KeyError:
    114

    115              def create_default_getter(field_name):
    116                  return lambda o:getattr(o, field_name)
    117

    118              from camelot.view.controls import delegates
    119              #

    120              # Default attributes for all fields

    121              #

    122              attributes = dict(

    123                  python_type = str,

    124                  getter = create_default_getter(field_name),

    125                  length = None,

    126                  tooltip = None,

    127                  background_color = None,

    128                  minimal_column_width = 12,

    129                  editable = False,

    130                  nullable = True,

    131                  widget = 'str',

    132                  blank = True,

    133                  delegate = delegates.PlainTextDelegate,

    134                  validator_list = [],

    135                  name = ugettext_lazy(field_name.replace('_', ' ').capitalize())

    136              )
    137

    138              #

    139              # Field attributes forced by the field_attributes property

    140              #

    141              forced_attributes = {}
    142              try:
    143                  forced_attributes = self.field_attributes[field_name]
    144              except KeyError:
    145                  pass
    146

    147              def get_entity_admin(target):
    148                  """Helper function that instantiated an Admin object for a

    149                  target entity class.

    150

    151                  :param target: an entity class for which an Admin object is

    152                  needed.

    153                  """
    154                  try:
    155                      fa = self.field_attributes[field_name]
    156                      target = fa.get('target', target)
    157                      admin_class = fa['admin']
    158                      return admin_class(self.app_admin, target)
    159                  except KeyError:
    160                      return self.get_related_entity_admin(target)
    161              #

    162              # Get the default field_attributes trough introspection if the

    163              # field is a mapped field

    164              #

    165              from sqlalchemy import orm
    166              from sqlalchemy.exceptions import InvalidRequestError
    167              from sqlalchemy.orm.exc import UnmappedClassError
    168              from field_attributes import _sqlalchemy_to_python_type_
    169              try:
    170                  mapper = orm.class_mapper(self.entity)
    171              except UnmappedClassError, exception:
    172                  from elixir import entities
    173                  mapped_entities = [str(e) for e in entities]
    174                  logger.error(u'%s is not a mapped class, mapped classes include %s'%(self.entity, u','.join([unicode(me) for me in mapped_entities])),

    175                               exc_info=exception)
    176                  raise exception
    177              try:
    178                  property = mapper.get_property(

    179                      field_name,

    180                      resolve_synonyms=True

    181                  )
    182                  if isinstance(property, orm.properties.ColumnProperty):
    183                      type = property.columns[0].type
    184                      python_type = _sqlalchemy_to_python_type_.get(

    185                          type.__class__,

    186                          None

    187                      )
    188                      if python_type:
    189                          attributes.update(python_type(type))
    190                      if not isinstance(

    191                          property.columns[0],

    192                          sqlalchemy.sql.expression._Label

    193                      ):
    194                          attributes['nullable'] = property.columns[0].nullable
    195                          attributes['default'] = property.columns[0].default
    196                  elif isinstance(property, orm.properties.PropertyLoader):
    197                      target = property._get_target().class_
    198                      foreign_keys = list(property._foreign_keys)
    199                      if property.direction == orm.interfaces.ONETOMANY:
    200                          attributes.update(

    201                              python_type = list,

    202                              editable = True,

    203                              nullable = True,

    204                              delegate = delegates.One2ManyDelegate,

    205                              target = target,

    206                              create_inline = False,

    207                              backref = property.backref.key,

    208                              direction = property.direction,

    209                              admin = get_entity_admin(target)

    210                          )
    211                      elif property.direction == orm.interfaces.MANYTOONE:
    212                          attributes.update(

    213                              python_type = str,

    214                              editable = True,

    215                              delegate = delegates.Many2OneDelegate,

    216                              target = target,

    217                              #

    218                              #@todo: take into account all foreign keys instead

    219                              # of only the first one

    220                              #

    221                              nullable = foreign_keys[0].nullable,

    222                              direction = property.direction,

    223                              admin = get_entity_admin(target)

    224                          )
    225                      elif property.direction == orm.interfaces.MANYTOMANY:
    226                          attributes.update(

    227                              python_type = list,

    228                              editable = True,

    229                              target = target,

    230                              nullable = True,

    231                              create_inline = False,

    232                              direction = property.direction,

    233                              delegate = delegates.ManyToManyDelegate,

    234                              admin = get_entity_admin(target)

    235                          )
    236                      else:
    237                          raise Exception('PropertyLoader has unknown direction')
    238              except InvalidRequestError:
    239                  #

    240                  # If the field name is not a property of the mapper, then use

    241                  # the default stuff

    242                  #

    243                  pass
    244

    245              if 'choices' in forced_attributes:
    246                  attributes['delegate'] = delegates.ComboBoxDelegate
    247                  attributes['editable'] = True
    248

    249              #

    250              # Overrule introspected field_attributes with those defined

    251              #

    252              attributes.update(forced_attributes)
    253

    254              #

    255              # In case of a 'target' field attribute, instantiate an appropriate

    256              # 'admin' attribute

    257              #

    258              if 'target' in attributes:
    259                  attributes['admin'] = get_entity_admin(attributes['target'])
    260

    261              self._field_attributes[field_name] = attributes
    262              return attributes
    263

    264      @model_function
    265 -    def get_list_charts(self):
    266          return self.list_charts
    267

    268      @model_function
    269 -    def get_filters(self):
    270          """Returns the filters applicable for these entities each filter is

    271

    272          :return: [(filter_name, [(option_name, query_decorator), ...), ... ]

    273          """
    274          from filters import structure_to_filter
    275

    276          def filter_generator():
    277              for structure in self.list_filter:
    278                  filter = structure_to_filter(structure)
    279                  yield (filter, filter.get_name_and_options(self))
    280

    281          return list(filter_generator())
    282

    283      @model_function
    284 -    def set_defaults(self, entity_instance, include_nullable_fields=True):
    285          """Set the defaults of an object

    286          :param include_nullable_fields: also set defaults for nullable fields, depending

    287          on the context, this should be set to False to allow the user to set the field

    288          to None

    289          """
    290          from sqlalchemy.schema import ColumnDefault
    291          for field, attributes in self.get_fields():
    292              has_default = False
    293              try:
    294                  default = attributes['default']
    295                  has_default = True
    296              except KeyError:
    297                  pass
    298              if has_default:
    299                  #

    300                  # prevent the setting of a default value when one has been

    301                  # set allready

    302                  #

    303                  value = attributes['getter'](entity_instance)
    304                  if value:
    305                      continue
    306                  if isinstance(default, ColumnDefault):
    307                      default_value = default.execute()
    308                  elif callable(default):
    309                      import inspect
    310                      args, _varargs, _kwargs, _defs = \

    311                          inspect.getargspec(default)
    312                      if len(args):
    313                          default_value = default(entity_instance)
    314                      else:
    315                          default_value = default()
    316                  else:
    317                      default_value = default
    318                  logger.debug(

    319                      'set default for %s to %s' % (

    320                          field,

    321                          unicode(default_value)

    322                      )

    323                  )
    324                  try:
    325                      setattr(entity_instance, field, default_value)
    326                  except AttributeError, e:
    327                      logger.error(

    328                          'Programming Error : could not set'

    329                          ' attribute %s to %s on %s' % (

    330                              field,

    331                              default_value,

    332                              entity_instance.__class__.__name__

    333                          ),

    334                          exc_info=e

    335                      )
    336

    337

    338      @gui_function
    339 -    def create_select_view(admin, query=None, search_text=None, parent=None):
    340          """Returns a Qt widget that can be used to select an element from a

    341          query

    342

    343          :param query: sqlalchemy query object

    344

    345          :param parent: the widget that will contain this select view, the

    346          returned widget has an entity_selected_signal signal that will be fired

    347          when a entity has been selected.

    348          """
    349          from controls.tableview import TableView
    350          from art import Icon
    351          from proxy.queryproxy import QueryTableProxy
    352          from PyQt4.QtCore import SIGNAL
    353

    354          class SelectQueryTableProxy(QueryTableProxy):
    355              header_icon = Icon('tango/16x16/emblems/emblem-symbolic-link.png')
    356

    357          class SelectView(TableView):
    358

    359              table_model = SelectQueryTableProxy
    360              title_format = 'Select %s'
    361

    362              def __init__(self, admin, parent):
    363                  TableView.__init__(

    364                      self, admin,

    365                      search_text=search_text, parent=parent

    366                  )
    367                  self.entity_selected_signal = SIGNAL('entity_selected')
    368                  self.connect(self, SIGNAL('row_selected'), self.sectionClicked)
    369                  self.setUpdatesEnabled(True)
    370

    371              def emit_and_close(self, instance_getter):
    372                  self.emit(self.entity_selected_signal, instance_getter)
    373                  from camelot.view.workspace import get_workspace
    374                  for window in get_workspace().subWindowList():
    375                      if hasattr(window, 'widget') and window.widget() == self:
    376                          window.close()
    377

    378              def sectionClicked(self, index):
    379                  # table model will be set by the model thread, we can't

    380                  # decently select if it has not been set yet

    381                  if self._table_model:
    382

    383                      def create_constant_getter(cst):
    384                          return lambda:cst
    385

    386                      def create_instance_getter():
    387                          entity = self._table_model._get_object(index)
    388                          return create_constant_getter(entity)
    389

    390                      post(create_instance_getter, self.emit_and_close)
    391

    392          widget = SelectView(admin, parent)
    393          widget.setUpdatesEnabled(True)
    394          widget.setMinimumSize(admin.list_size[0], admin.list_size[1])
    395          widget.update()
    396          return widget
    397

    398      @gui_function
    399 -    def create_table_view(self, query_getter=None, parent=None):
    400          """Returns a Qt widget containing a table view, for a certain query,

    401          using this Admin class; the table widget contains a model

    402          QueryTableModel

    403

    404          :param query_getter: sqlalchemy query object

    405

    406          :param parent: the workspace widget that will contain the table view

    407          """
    408

    409          from PyQt4 import QtCore
    410          from PyQt4.QtCore import SIGNAL
    411

    412          from proxy.queryproxy import QueryTableProxy
    413          tableview = self.TableView(self)
    414

    415          def createOpenForm(self, tableview):
    416

    417              def openForm(index):
    418                  from workspace import get_workspace
    419                  model = QueryTableProxy(

    420                      tableview.admin,

    421                      tableview._table_model.get_query_getter(),

    422                      tableview.admin.get_fields,

    423                      max_number_of_rows=1

    424                  )
    425                  title = ''
    426                  formview = tableview.admin.create_form_view(

    427                      title, model, index, parent

    428                  )
    429                  get_workspace().addSubWindow(formview)
    430                  formview.show()
    431

    432              return openForm
    433

    434          tableview.connect(

    435              tableview,

    436              SIGNAL('row_selected'),

    437              createOpenForm(self, tableview)

    438          )
    439

    440          return tableview
    441

    442      @model_function
    443 -    def delete(self, entity_instance):
    444          """Delete an entity instance"""
    445          from sqlalchemy.orm.session import Session
    446          session = Session.object_session( entity_instance )
    447          #

    448          # new and deleted instances cannot be deleted

    449          #

    450          if session:
    451              if entity_instance in session.new:
    452                  session.expunge(entity_instance)
    453              elif (entity_instance not in session.deleted) and \

    454                   (entity_instance in session): # if the object is not in the session, it might allready be deleted
    455                  history = None
    456                  #

    457                  # only if we know the primary key, we can keep track of its history

    458                  #

    459                  if hasattr(entity_instance, 'id') and entity_instance.id:
    460                      pk = entity_instance.id
    461                      # save the state before the update

    462                      from camelot.model.memento import BeforeDelete
    463                      from camelot.model.authentication import getCurrentAuthentication
    464                      history = BeforeDelete( model = unicode( self.entity.__name__ ),

    465                                              primary_key = pk,

    466                                              previous_attributes = {},

    467                                              authentication = getCurrentAuthentication() )
    468                  entity_instance.delete()
    469                  session.flush( [entity_instance] )
    470                  if history:
    471                      Session.object_session( history ).flush( [history] )
    472

    473      @model_function
    474 -    def flush(self, entity_instance):
    475          """Flush the pending changes of this entity instance to the backend"""
    476          from sqlalchemy.orm.session import Session
    477          session = Session.object_session( entity_instance )
    478          if not session:
    479              logger.error('Programming Error : entity %s cannot be flushed because it has no session'%(unicode(entity_instance)))
    480          session.flush( [entity_instance] )
    481

    482

    483      @model_function
    484 -    def copy(self, entity_instance):
    485          """Duplicate this entity instance"""
    486          new_entity_instance = entity_instance.__class__()
    487          new_entity_instance.from_dict( entity_instance.to_dict(exclude=['id']) )
    488          return new_entity_instance
    489