Package Camelot :: Package camelot :: Package admin :: Module object_admin |
[frames] | no frames]

# Source Code for Module Camelot.camelot.admin.object_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  """Admin class for Plain Old Python Object"""
     29
     30  import logging
     31  logger = logging.getLogger('camelot.view.object_admin')
     32
     33  from camelot.view.model_thread import gui_function, model_function
     34  from camelot.core.utils import ugettext as _
     35  from camelot.core.utils import ugettext_lazy
     36  from camelot.view.proxy.collection_proxy import CollectionProxy
     37  from validator.object_validator import ObjectValidator
     38
     39
     40 -class ObjectAdmin(object):
     41      """The ObjectAdmin class describes the interface that will be used
     42      to interact with objects of a certain class.  The behaviour of this class
     43      and the resulting interface can be tuned by specifying specific class
     44      attributes:
     45
     46      .. attribute:: verbose_name
     47
     48      A human-readable name for the object, singular ::
     49
     50      verbose_name = 'movie'
     51
     52      If this isn't given, the class name will be used
     53
     54      .. attribute:: verbose_name_plural
     55
     56      A human-readable name for the object, plural ::
     57
     58      verbose_name_plural = 'movies'
     59
     60      If this isn't given, Camelot will use verbose_name + "s"
     61
     62      .. attribute:: list_display
     63
     64      a list with the fields that should be displayed in a table view
     65
     66      .. attribute:: form_display
     67
     68      a list with the fields that should be displayed in a form view, defaults to
     69      the same fields as those specified in list_display ::
     70
     71      class Admin(EntityAdmin):
     72        form_display = ['title', 'rating', 'cover']
     73
     74      instead of telling which forms to display. It is also possible to define
     75      the form itself ::
     76
     77      from camelot.view.forms import Form, TabForm, WidgetOnlyForm, HBoxForm
     78
     79      class Admin(EntityAdmin):
     80        form_display = TabForm([
     81          ('Movie', Form([
     82            HBoxForm([['title', 'rating'], WidgetOnlyForm('cover')]),
     83            'short_description',
     84            'releasedate',
     85            'director',
     86            'script',
     87            'genre',
     88            'description', 'tags'], scrollbars=True)),
     89          ('Cast', WidgetOnlyForm('cast'))
     90        ])
     91
     92
     93      .. attribute:: list_filter
     94
     95      A list of fields that should be used to generate filters for in the table
     96      view.  If the field named is a one2many, many2one or many2many field, the
     97      field name should be followed by a field name of the related entity ::
     98
     99      class Project(Entity):
    100        oranization = OneToMany('Organization')
    101        name = Field(Unicode(50))
    102
    103        class Admin(EntityAdmin):
    104          list_display = ['organization']
    105          list_filter = ['organization.name']
    106
    107      .. image:: ../_static/filter/group_box_filter.png
    108
    109      .. attribute:: list_search
    110
    111      A list of fields that should be searched when the user enters something in
    112      the search box in the table view.  By default only character fields are
    113      searched.  For use with one2many, many2one or many2many fields, the same
    114      rules as for the list_filter attribute apply
    115
    116      .. attribute:: confirm_delete
    117
    118      Indicates if the deletion of an object should be confirmed by the user, defaults
    119      to False.  Can be set to either True, False, or the message to display when asking
    120      confirmation of the deletion.
    121
    122      .. attribute:: form_size
    123
    124      a tuple indicating the size of a form view, defaults to (700,500)
    125
    126      .. attribute:: form_actions
    127
    128      Actions to be accessible by pushbuttons on the side of a form,
    129      a list of tuples (button_label, action_function) where action_function
    130      takes as its single argument, a method that returns the the object that
    131      was displayed by the form when the button was pressed::
    132
    133      class Admin(EntityAdmin):
    134        form_actions = [('Foo', lamda o_getter:print 'foo')]
    135
    136      .. attribute:: field_attributes
    137
    138      A dictionary specifying for each field of the model some additional
    139      attributes on how they should be displayed.  All of these attributes
    140      are propagated to the constructor of the delegate of this field::
    141
    142      class Movie(Entity):
    143        title = Field(Unicode(50))
    144
    145        class Admin(EntityAdmin):
    146          list_display = ['title']
    147          field_attributes = dict(title=dict(editable=False))
    148
    149      Other field attributes process by the admin interface are:
    150
    151      .. attribute:: name
    152      The name of the field used, this defaults to the name of the attribute
    153
    154      .. attribute:: target
    155      In case of relation fields, specifies the class that is at the other
    156      end of the relation.  Defaults to the one found by introspection.
    157
    158      .. attribute:: admin
    159      In case of relation fields, specifies the admin class that is to be used
    160      to visualize the other end of the relation.  Defaults to the default admin
    161      class of the target class.
    162
    163      .. attribute:: model
    164      The QAbstractItemModel class to be used to display collections of this object,
    165      defaults to a CollectionProxy
    166
    167      .. attribute:: confirm_delete
    168      set to True if the user should get a confirmation dialog before deleting data,
    169      defaults to False
    170
    171      .. attribute:: TableView
    172      The QWidget class to be used when a table view is needed
    173      """
    174      name = None #DEPRECATED
    175      verbose_name = None
    176      verbose_name_plural = None
    177      list_display = []
    178      validator = ObjectValidator
    179      model = CollectionProxy
    180      fields = []
    181      form = [] #DEPRECATED
    182      form_display = []
    183      list_filter = []
    184      list_charts = []
    185      list_actions = []
    186      list_search = []
    187      confirm_delete = False
    188      list_size = (600, 400)
    189      form_size = (700, 500)
    190      form_actions = []
    191      form_title_column = None #DEPRECATED
    192      field_attributes = {}
    193
    194      TableView = None
    195
    196 -    def __init__(self, app_admin, entity):
    197          """
    198
    199          :param app_admin: the application admin object for this application, if None,
    200          then the default application_admin is taken
    201          :param entity: the entity class for which this admin instance is to be
    202          used
    203          """
    204          from camelot.view.remote_signals import get_signal_handler
    205          from camelot.view.controls.tableview import TableView
    206          if not self.TableView:
    207              self.TableView = TableView
    208          if not app_admin:
    209              from camelot.view.application_admin import get_application_admin
    210              self.app_admin = get_application_admin()
    211          else:
    212              self.app_admin = app_admin
    213          self.rsh = get_signal_handler()
    214          if entity:
    215              from camelot.view.model_thread import get_model_thread
    216              self.entity = entity
    217              self.mt = get_model_thread()
    218          #
    219          # caches to prevent recalculation of things
    220          #
    221          self._field_attributes = dict()
    222          self._subclasses = None
    223
    224 -    def __str__(self):
    225          return 'Admin %s' % str(self.entity.__name__)
    226
    227 -    def __repr__(self):
    228          return 'ObjectAdmin(%s)' % str(self.entity.__name__)
    229
    230 -    def get_name(self):
    231          return self.get_verbose_name()
    232
    233 -    def get_verbose_name(self):
    234          return unicode(
    235              self.verbose_name or self.name or _(self.entity.__name__.capitalize())
    236          )
    237
    238 -    def get_verbose_name_plural(self):
    239          return unicode(
    240              self.verbose_name_plural
    241              or self.name
    242              or (self.get_verbose_name() + 's')
    243          )
    244
    245      @model_function
    246 -    def get_verbose_identifier(self, obj):
    247          """Create an identifier for an object that is interpretable
    248          for the user, eg : the 'id' of an object.  This verbose identifier can
    249          be used to generate a title for a form view of an object.
    250          """
    251          return u'%s : %s' % (self.get_verbose_name(), unicode(obj))
    252
    253 -    def get_entity_admin(self, entity):
    254          return self.app_admin.get_entity_admin(entity)
    255
    256 -    def get_confirm_delete(self):
    257          if self.confirm_delete:
    258              if self.confirm_delete==True:
    259                  return _('Are you sure you want to delete this')
    260              return self.confirm_delete
    261          return False
    262
    263      @model_function
    264 -    def get_form_actions(self, entity):
    265          from camelot.admin.form_action import structure_to_form_actions
    266          return structure_to_form_actions(self.form_actions)
    267
    268      @model_function
    269 -    def get_list_actions(self):
    270          from camelot.admin.list_action import structure_to_list_actions
    271          return structure_to_list_actions(self.list_actions)
    272
    273      @model_function
    274 -    def get_subclass_tree( self ):
    275          """Get a tree of admin classes representing the subclasses of the class
    276          represented by this admin class
    277
    278          :return: [(subclass_admin, [(subsubclass_admin, [...]),...]),...]
    279          """
    280          subclasses = []
    281          for subclass in self.entity.__subclasses__():
    282              subclass_admin = self.get_related_entity_admin(subclass)
    283              subclasses.append((
    284                  subclass_admin,
    285                  subclass_admin.get_subclass_tree()
    286              ))
    287
    288          def sort_admins(a1, a2):
    289              return cmp(a1[0].get_verbose_name_plural(), a2[0].get_verbose_name_plural())
    290
    291          subclasses.sort(cmp=sort_admins)
    292          return subclasses
    293
    294 -    def get_related_entity_admin(self, entity):
    295          """Get an admin object for another entity.  Taking into account
    296          preferences of this admin object or for those of admin object higher up
    297          the chain such as the application admin object.
    298
    299          :param entity: the entity class for which an admin object is requested
    300          """
    301          if entity==self.entity:
    302              return self
    303          related_admin = self.app_admin.get_entity_admin(entity)
    304          if not related_admin:
    305              logger.warn('no related admin found for %s' % (entity.__name__))
    306          return related_admin
    307
    308 -    def get_field_attributes(self, field_name):
    309          """Get the attributes needed to visualize the field field_name
    310
    311          :param field_name : the name of the field
    312
    313          :return: a dictionary of attributes needed to visualize the field,
    314          those attributes can be:
    315           * python_type : the corresponding python type of the object
    316           * editable : bool specifying wether the user can edit this field
    317           * widget : which widget to be used to render the field
    318           * ...
    319          """
    320          try:
    321              return self._field_attributes[field_name]
    322          except KeyError:
    323
    324              def create_default_getter(field_name):
    325                  return lambda o:getattr(o, field_name)
    326
    327              from camelot.view.controls import delegates
    328              #
    329              # Default attributes for all fields
    330              #
    331              attributes = dict(
    332                  getter=create_default_getter(field_name),
    333                  python_type=str,
    334                  length=None,
    335                  tooltip=None,
    336                  background_color=None,
    337                  minimal_column_width=12,
    338                  editable=False,
    339                  nullable=True,
    340                  widget='str',
    341                  blank=True,
    342                  delegate=delegates.PlainTextDelegate,
    343                  validator_list=[],
    344                  name=ugettext_lazy(field_name.replace( '_', ' ' ).capitalize())
    345              )
    346              #
    347              # Field attributes forced by the field_attributes property
    348              #
    349              forced_attributes = {}
    350              try:
    351                  forced_attributes = self.field_attributes[field_name]
    352              except KeyError:
    353                  pass
    354
    355              #
    356              # TODO : move part of logic from entity admin class over here
    357              #
    358
    359              #
    360              # Overrule introspected field_attributes with those defined
    361              #
    362              attributes.update(forced_attributes)
    363
    364              #
    365              # In case of a 'target' field attribute, instantiate an appropriate
    366              # 'admin' attribute
    367              #
    368
    369              def get_entity_admin(target):
    370                  """Helper function that instantiated an Admin object for a
    371                  target entity class
    372
    373                  :param target: an entity class for which an Admin object is
    374                  needed
    375                  """
    376                  try:
    377                      fa = self.field_attributes[field_name]
    378                      target = fa.get('target', target)
    379                      admin_class = fa['admin']
    380                      return admin_class(self.app_admin, target)
    381                  except KeyError:
    382                      return self.get_related_entity_admin(target)
    383
    384              if 'target' in attributes:
    385                  attributes['admin'] = get_entity_admin(attributes['target'])
    386
    387              self._field_attributes[field_name] = attributes
    388              return attributes
    389
    390      @model_function
    391 -    def get_columns(self):
    392          """
    393          The columns to be displayed in the list view, returns a list of pairs
    394          of the name of the field and its attributes needed to display it
    395          properly
    396
    397          @return: [(field_name,
    398                    {'widget': widget_type,
    399                     'editable': True or False,
    400                     'blank': True or False,
    401                     'validator_list':[...],
    402                     'name':'Field name'}),
    403                   ...]
    404          """
    405          return [(field, self.get_field_attributes(field))
    406                  for field in self.list_display]
    407
    408 -    def create_validator(self, model):
    409          return self.validator(self, model)
    410
    411      @model_function
    412 -    def get_fields(self):
    413          if self.form or self.form_display:
    414              fields = self.get_form_display().get_fields()
    415          elif self.fields:
    416              fields = self.fields
    417          else:
    418              fields = self.list_display
    419          fields_and_attributes =  [
    420                  (field, self.get_field_attributes(field))
    421                  for field in fields
    422          ]
    423          return fields_and_attributes
    424
    425      @model_function
    426 -    def get_all_fields_and_attributes(self):
    427          """A dictionary of (field_name:field_attributes) for all fields that can
    428          possibly appear in a list or a form or for which field attributes have
    429          been defined
    430          """
    431          fields = dict(self.get_columns())
    432          fields.update(dict(self.get_fields()))
    433          return fields
    434
    435      @model_function
    436 -    def get_form_display(self):
    437          from camelot.view.forms import Form, structure_to_form
    438          if self.form or self.form_display:
    439              return structure_to_form(self.form or self.form_display)
    440          if self.list_display:
    441              return Form(self.list_display)
    442          return Form([])
    443
    444      @gui_function
    445 -    def create_form_view(self, title, model, index, parent=None):
    446          """Creates a Qt widget containing a form view, for a specific index in
    447          a model.  Use this method to create a form view for a collection of objects,
    448          the user will be able to use PgUp/PgDown to move to the next object.
    449
    450          :param title: the title of the form view
    451          :param model: the data model to be used to fill the form view
    452          :param index: which row in the data model to display
    453          :param parent: the parent widget for the form
    454          """
    455          logger.debug('creating form view for index %s' % index)
    456          from camelot.view.controls.formview import FormView
    457          form = FormView(title, self, model, index)
    458          return form
    459
    460 -    def set_defaults(self, object_instance, include_nullable_fields=True):
    461          pass
    462
    463      @gui_function
    464 -    def create_object_form_view(self, title, object_getter, parent=None):
    465          """Create a form view for a single object, PgUp/PgDown will do
    466          nothing.
    467
    468          :param title: the title of the form view
    469          :param object_getter: a function taking no arguments, and returning the object
    470          :param parent: the parent widget for the form
    471          """
    472
    473          def create_collection_getter( object_getter ):
    474              return lambda:[object_getter()]
    475
    476          model = self.model( self,
    477                              create_collection_getter( object_getter ),
    478                              self.get_fields )
    479          return self.create_form_view( title, model, 0, parent )
    480
    481      @gui_function
    482 -    def create_new_view(admin, parent=None, oncreate=None, onexpunge=None):
    483          """Create a Qt widget containing a form to create a new instance of the
    484          entity related to this admin class
    485
    486          The returned class has an 'entity_created_signal' that will be fired
    487          when a valid new entity was created by the form
    488          """
    489          from PyQt4 import QtCore
    490          from PyQt4 import QtGui
    491          from PyQt4.QtCore import SIGNAL
    492          from camelot.view.controls.view import AbstractView
    493          from camelot.view.model_thread import post
    494          from camelot.view.proxy.collection_proxy import CollectionProxy
    495          new_object = []
    496
    497          @model_function
    498          def collection_getter():
    499              if not new_object:
    500                  entity_instance = admin.entity()
    501                  if oncreate:
    502                      oncreate(entity_instance)
    503                  # Give the default fields their value
    504                  admin.set_defaults(entity_instance)
    505                  new_object.append(entity_instance)
    506              return new_object
    507
    508          model = CollectionProxy(
    509              admin,
    510              collection_getter,
    511              admin.get_fields,
    512              max_number_of_rows=1
    513          )
    514          validator = admin.create_validator(model)
    515
    516          class NewForm(AbstractView):
    517
    518              def __init__(self, parent):
    519                  AbstractView.__init__(self, parent)
    520                  self.widget_layout = QtGui.QVBoxLayout()
    521                  self.widget_layout.setMargin(0)
    522                  title = _('new')
    523                  index = 0
    524                  self.form_view = admin.create_form_view(
    525                      title, model, index, parent
    526                  )
    527                  self.widget_layout.insertWidget(0, self.form_view)
    528                  self.setLayout(self.widget_layout)
    529                  self.validate_before_close = True
    530                  self.entity_created_signal = SIGNAL('entity_created')
    531                  #
    532                  # every time data has been changed, it could become valid,
    533                  # when this is the case, it should be propagated
    534                  #
    535                  self.connect(
    536                      model,
    537                      SIGNAL(
    538                          'dataChanged(const QModelIndex &, const QModelIndex &)'
    539                      ),
    540                      self.dataChanged
    541                  )
    542                  self.connect(
    543                      self.form_view,
    544                      AbstractView.title_changed_signal,
    545                      self.change_title
    546                  )
    547
    548              def emit_if_valid(self, valid):
    549                  if valid:
    550
    551                      def create_instance_getter(new_object):
    552                          return lambda:new_object[0]
    553
    554                      self.emit(
    555                          self.entity_created_signal,
    556                          create_instance_getter(new_object)
    557                      )
    558
    559              def dataChanged(self, index1, index2):
    560
    561                  def validate():
    562                      return validator.isValid(0)
    563
    564                  post(validate, self.emit_if_valid)
    565
    566              def showMessage(self, valid):
    567                  from camelot.view.workspace import get_workspace
    568                  if not valid:
    569                      row = 0
    570                      reply = validator.validityDialog(row, self).exec_()
    571                      if reply == QtGui.QMessageBox.Discard:
    572                          # clear mapping to prevent data being written again to
    573                          # the model, after we reverted the row
    574                          self.form_view._form.clear_mapping()
    575
    576                          def onexpunge_on_all():
    577                              if onexpunge:
    578                                  for o in new_object:
    579                                      onexpunge(o)
    580
    581                          post(onexpunge_on_all)
    582                          self.validate_before_close = False
    583
    584                          for window in get_workspace().subWindowList():
    585                              if window.widget() == self:
    586                                  window.close()
    587                  else:
    588                      def create_instance_getter(new_object):
    589                          return lambda:new_object[0]
    590
    591                      for _o in new_object:
    592                          self.emit(
    593                              self.entity_created_signal,
    594                              create_instance_getter(new_object)
    595                          )
    596                      self.validate_before_close = False
    597                      from camelot.view.workspace import NoDesktopWorkspace
    598                      workspace = get_workspace()
    599                      if isinstance(workspace, (NoDesktopWorkspace,)):
    600                          self.close()
    601                      else:
    602                          for window in get_workspace().subWindowList():
    603                              if window.widget() == self:
    604                                  window.close()
    605
    606              def validateClose(self):
    607                  logger.debug(
    608                      'validate before close : %s' %
    609                      self.validate_before_close
    610                  )
    611                  if self.validate_before_close:
    612                      self.form_view._form.submit()
    613                      logger.debug(
    614                          'unflushed rows : %s' %
    615                          str(model.hasUnflushedRows())
    616                      )
    617                      if model.hasUnflushedRows():
    618                          def validate(): return validator.isValid(0)
    619                          post(validate, self.showMessage)
    620                          return False
    621                      else:
    622                          return True
    623                  return True
    624
    625              def closeEvent(self, event):
    626                  if self.validateClose():
    627                      event.accept()
    628                  else:
    629                      event.ignore()
    630
    631          form = NewForm(parent)
    632          if hasattr(admin, 'form_size'):
    633              form.setMinimumSize(admin.form_size[0], admin.form_size[1])
    634          return form
    635
    636      @model_function
    637 -    def delete(self, entity_instance):
    638          """Delete an entity instance"""
    639          del entity_instance
    640
    641      @model_function
    642 -    def flush(self, entity_instance):
    643          """Flush the pending changes of this entity instance to the backend"""
    644          pass
    645
    646      @model_function
    647 -    def add(self, entity_instance):
    648          """Add an entity instance as a managed entity instance"""
    649          pass
    650
    651      @model_function
    652 -    def copy(self, entity_instance):
    653          """Duplicate this entity instance"""
    654          new_entity_instance = entity_instance.__class__()
    655          return new_entity_instance
    656