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):
267 268 @model_function
269 - def get_list_actions(self):
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 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