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