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 """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
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
175 verbose_name = None
176 verbose_name_plural = None
177 list_display = []
178 validator = ObjectValidator
179 model = CollectionProxy
180 fields = []
181 form = []
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
192 field_attributes = {}
193
194 TableView = None
195
223
225 return 'Admin %s' % str(self.entity.__name__)
226
228 return 'ObjectAdmin(%s)' % str(self.entity.__name__)
229
232
234 return unicode(
235 self.verbose_name or self.name or _(self.entity.__name__.capitalize())
236 )
237
244
245 @model_function
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
255
262
263 @model_function
267
268 @model_function
272
273 @model_function
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
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
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
348
349 forced_attributes = {}
350 try:
351 forced_attributes = self.field_attributes[field_name]
352 except KeyError:
353 pass
354
355
356
357
358
359
360
361
362 attributes.update(forced_attributes)
363
364
365
366
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
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
410
411 @model_function
424
425 @model_function
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
443
444 @gui_function
459
460 - def set_defaults(self, object_instance, include_nullable_fields=True):
462
463 @gui_function
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
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
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
533
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
573
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