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