1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
60 name = None
61 list_display = []
62 fields = []
63 form = []
64 form_display = []
65
66
67
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
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
91
92 self.__field_attributes = dict()
93
95 return 'Admin %s' % str(self.entity.__name__)
96
98 return (self.name or self.entity.__name__)
99
102
103 @model_function
106
116
117 @model_function
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
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
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
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
247
253
254 @model_function
257
258 @model_function
271
272 return list(filter_generator())
273
277
278 @model_function
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
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
359
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
411
412 @gui_function
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
435
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
491
492 return openForm
493
494 tableview.connect(tableview,
495 QtCore.SIGNAL('row_selected'),
496 createOpenForm(self, tableview))
497
498 return tableview
499