root/trunk/camelot/admin/object_admin.py @ 1178

Revision 1178, 22.4 KB (checked in by erikj, 6 months ago)

add confirm_delete class attribute to ObjectAdmin?

Line 
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
30import logging
31logger = logging.getLogger('camelot.view.object_admin')
32
33from camelot.view.model_thread import gui_function, model_function
34from camelot.core.utils import ugettext as _
35from camelot.core.utils import ugettext_lazy
36from camelot.view.proxy.collection_proxy import CollectionProxy
37from validator.object_validator import ObjectValidator
38
39
40class 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    name = None #DEPRECATED
168    verbose_name = None
169    verbose_name_plural = None
170    list_display = []
171    validator = ObjectValidator
172    model = CollectionProxy
173    fields = []
174    form = [] #DEPRECATED
175    form_display = []
176    list_filter = []
177    list_charts = []
178    list_actions = []
179    list_search = []
180    confirm_delete = False
181    list_size = (600, 400)
182    form_size = (700, 500)
183    form_actions = []
184    form_title_column = None #DEPRECATED
185    field_attributes = {}
186
187    def __init__(self, app_admin, entity):
188        """
189       
190        :param app_admin: the application admin object for this application, if None,
191        then the default application_admin is taken
192        :param entity: the entity class for which this admin instance is to be
193        used
194        """
195        from camelot.view.remote_signals import get_signal_handler
196        if not app_admin:
197            from camelot.view.application_admin import get_application_admin
198            self.app_admin = get_application_admin()
199        else:
200            self.app_admin = app_admin
201        self.rsh = get_signal_handler()
202        if entity:
203            from camelot.view.model_thread import get_model_thread
204            self.entity = entity
205            self.mt = get_model_thread()
206        #
207        # caches to prevent recalculation of things
208        #
209        self._field_attributes = dict()
210        self._subclasses = None
211
212    def __str__(self):
213        return 'Admin %s' % str(self.entity.__name__)
214
215    def __repr__(self):
216        return 'ObjectAdmin(%s)' % str(self.entity.__name__)
217
218    def get_name(self):
219        return self.get_verbose_name()
220
221    def get_verbose_name(self):
222        return unicode(
223            self.verbose_name or self.name or _(self.entity.__name__.capitalize())
224        )
225
226    def get_verbose_name_plural(self):
227        return unicode(
228            self.verbose_name_plural
229            or self.name
230            or (self.get_verbose_name() + 's')
231        )
232
233    @model_function
234    def get_verbose_identifier(self, obj):
235        """Create an identifier for an object that is interpretable
236        for the user, eg : the 'id' of an object.  This verbose identifier can
237        be used to generate a title for a form view of an object.
238        """
239        return u'%s : %s' % (self.get_verbose_name(), unicode(obj))
240
241    def get_entity_admin(self, entity):
242        return self.app_admin.get_entity_admin(entity)
243   
244    def get_confirm_delete(self):
245        if self.confirm_delete:
246            if self.confirm_delete==True:
247                return _('Are you sure you want to delete this')
248            return self.confirm_delete
249        return False
250
251    @model_function
252    def get_form_actions(self, entity):
253        from camelot.admin.form_action import structure_to_form_actions
254        return structure_to_form_actions(self.form_actions)
255
256    @model_function
257    def get_list_actions(self):
258        from camelot.admin.list_action import structure_to_list_actions
259        return structure_to_list_actions(self.list_actions)
260
261    @model_function
262    def get_subclass_tree( self ):
263        """Get a tree of admin classes representing the subclasses of the class
264        represented by this admin class
265       
266        :return: [(subclass_admin, [(subsubclass_admin, [...]),...]),...]
267        """
268        subclasses = []
269        for subclass in self.entity.__subclasses__():
270            subclass_admin = self.get_related_entity_admin(subclass)
271            subclasses.append((
272                subclass_admin,
273                subclass_admin.get_subclass_tree()
274            ))
275           
276        def sort_admins(a1, a2):
277            return cmp(a1[0].get_verbose_name_plural(), a2[0].get_verbose_name_plural())
278       
279        subclasses.sort(cmp=sort_admins)
280        return subclasses
281
282    def get_related_entity_admin(self, entity):
283        """Get an admin object for another entity.  Taking into account
284        preferences of this admin object or for those of admin object higher up
285        the chain such as the application admin object.
286
287        :param entity: the entity class for which an admin object is requested
288        """
289        if entity==self.entity:
290            return self
291        related_admin = self.app_admin.get_entity_admin(entity)
292        if not related_admin:
293            logger.warn('no related admin found for %s' % (entity.__name__))
294        return related_admin
295
296    def get_field_attributes(self, field_name):
297        """Get the attributes needed to visualize the field field_name
298       
299        :param field_name : the name of the field
300       
301        :return: a dictionary of attributes needed to visualize the field,
302        those attributes can be:
303         * python_type : the corresponding python type of the object
304         * editable : bool specifying wether the user can edit this field
305         * widget : which widget to be used to render the field
306         * ...
307        """
308        try:
309            return self._field_attributes[field_name]
310        except KeyError:
311            from camelot.view.controls import delegates
312            #
313            # Default attributes for all fields
314            #
315            attributes = dict(
316                python_type=str,
317                length=None,
318                tooltip=None,
319                background_color=None,
320                minimal_column_width=0,
321                editable=False,
322                nullable=True,
323                widget='str',
324                blank=True,
325                delegate=delegates.PlainTextDelegate,
326                validator_list=[],
327                name=ugettext_lazy(field_name.replace( '_', ' ' ).capitalize())
328            )
329            #
330            # Field attributes forced by the field_attributes property
331            #
332            forced_attributes = {}
333            try:
334                forced_attributes = self.field_attributes[field_name]
335            except KeyError:
336                pass
337
338            #
339            # TODO : move part of logic from entity admin class over here
340            #
341
342            #
343            # Overrule introspected field_attributes with those defined
344            #
345            attributes.update(forced_attributes)
346
347            #
348            # In case of a 'target' field attribute, instantiate an appropriate
349            # 'admin' attribute
350            #
351
352            def get_entity_admin(target):
353                """Helper function that instantiated an Admin object for a
354                target entity class
355
356                :param target: an entity class for which an Admin object is
357                needed
358                """
359                try:
360                    fa = self.field_attributes[field_name]
361                    target = fa.get('target', target)
362                    admin_class = fa['admin']
363                    return admin_class(self.app_admin, target)
364                except KeyError:
365                    return self.get_related_entity_admin(target)
366
367            if 'target' in attributes:
368                attributes['admin'] = get_entity_admin(attributes['target'])
369
370            self._field_attributes[field_name] = attributes
371            return attributes
372
373    @model_function
374    def get_columns(self):
375        """
376        The columns to be displayed in the list view, returns a list of pairs
377        of the name of the field and its attributes needed to display it
378        properly
379
380        @return: [(field_name,
381                  {'widget': widget_type,
382                   'editable': True or False,
383                   'blank': True or False,
384                   'validator_list':[...],
385                   'name':'Field name'}),
386                 ...]
387        """
388        return [(field, self.get_field_attributes(field))
389                for field in self.list_display]
390
391    def create_validator(self, model):
392        return self.validator(self, model)
393
394    @model_function
395    def get_fields(self):
396        if self.form or self.form_display:
397            fields = self.get_form_display().get_fields()
398        elif self.fields:
399            fields = self.fields
400        else:
401            fields = self.list_display
402        fields_and_attributes =  [
403                (field, self.get_field_attributes(field))
404                for field in fields
405        ]
406        return fields_and_attributes
407
408    @model_function
409    def get_all_fields_and_attributes(self):
410        """A list of (field_name, field_attributes) for all fields that can
411        possibly appear in a list or a form or for which field attributes have
412        been defined
413        """
414        pass
415
416    @model_function
417    def get_form_display(self):
418        from camelot.view.forms import Form, structure_to_form
419        if self.form or self.form_display:
420            return structure_to_form(self.form or self.form_display)
421        if self.list_display:
422            return Form(self.list_display)
423        return Form([])
424
425    @gui_function
426    def create_form_view(self, title, model, index, parent=None):
427        """Creates a Qt widget containing a form view, for a specific index in
428        a model.  Use this method to create a form view for a collection of objects,
429        the user will be able to use PgUp/PgDown to move to the next object.
430       
431        :param title: the title of the form view
432        :param model: the data model to be used to fill the form view
433        :param index: which row in the data model to display
434        :param parent: the parent widget for the form
435        """
436        logger.debug('creating form view for index %s' % index)
437        from camelot.view.controls.formview import FormView
438        form = FormView(title, self, model, index)
439        return form
440   
441    def set_defaults(self, object_instance):
442        pass
443   
444    @gui_function
445    def create_object_form_view(self, title, object_getter, parent=None):
446        """Create a form view for a single object, PgUp/PgDown will do
447        nothing.
448       
449        :param title: the title of the form view
450        :param object_getter: a function taking no arguments, and returning the object
451        :param parent: the parent widget for the form
452        """
453       
454        def create_collection_getter( object_getter ):
455            return lambda:[object_getter()]
456                   
457        model = self.model( self,
458                            create_collection_getter( object_getter ),
459                            self.get_fields )
460        return self.create_form_view( title, model, 0, parent )
461   
462    @gui_function
463    def create_new_view(admin, parent=None, oncreate=None, onexpunge=None):
464        """Create a Qt widget containing a form to create a new instance of the
465        entity related to this admin class
466
467        The returned class has an 'entity_created_signal' that will be fired
468        when a valid new entity was created by the form
469        """
470        from PyQt4 import QtCore
471        from PyQt4 import QtGui
472        from PyQt4.QtCore import SIGNAL
473        from camelot.view.controls.view import AbstractView
474        from camelot.view.model_thread import post
475        from camelot.view.proxy.collection_proxy import CollectionProxy
476        new_object = []
477
478        @model_function
479        def collection_getter():
480            if not new_object:
481                entity_instance = admin.entity()
482                if oncreate:
483                    oncreate(entity_instance)
484                # Give the default fields their value
485                admin.set_defaults(entity_instance)
486                new_object.append(entity_instance)
487            return new_object
488
489        model = CollectionProxy(
490            admin,
491            collection_getter,
492            admin.get_fields,
493            max_number_of_rows=1
494        )
495        validator = admin.create_validator(model)
496
497        class NewForm(AbstractView):
498
499            def __init__(self, parent):
500                AbstractView.__init__(self, parent)
501                self.widget_layout = QtGui.QVBoxLayout()
502                self.widget_layout.setMargin(0)
503                title = _('new')
504                index = 0
505                self.form_view = admin.create_form_view(
506                    title, model, index, parent
507                )
508                self.widget_layout.insertWidget(0, self.form_view)
509                self.setLayout(self.widget_layout)
510                self.validate_before_close = True
511                self.entity_created_signal = SIGNAL('entity_created')
512                #
513                # every time data has been changed, it could become valid,
514                # when this is the case, it should be propagated
515                #
516                self.connect(
517                    model,
518                    SIGNAL(
519                        'dataChanged(const QModelIndex &, const QModelIndex &)'
520                    ),
521                    self.dataChanged
522                )
523                self.connect(
524                    self.form_view,
525                    AbstractView.title_changed_signal,
526                    self.change_title
527                )
528
529            def emit_if_valid(self, valid):
530                if valid:
531
532                    def create_instance_getter(new_object):
533                        return lambda:new_object[0]
534
535                    self.emit(
536                        self.entity_created_signal,
537                        create_instance_getter(new_object)
538                    )
539
540            def dataChanged(self, index1, index2):
541
542                def validate():
543                    return validator.isValid(0)
544
545                post(validate, self.emit_if_valid)
546
547            def showMessage(self, valid):
548                from camelot.view.workspace import get_workspace
549                if not valid:
550                    row = 0
551                    reply = validator.validityDialog(row, self).exec_()
552                    if reply == QtGui.QMessageBox.Discard:
553                        # clear mapping to prevent data being written again to
554                        # the model, after we reverted the row
555                        self.form_view.widget_mapper.clearMapping()
556
557                        def onexpunge_on_all():
558                            if onexpunge:
559                                for o in new_object:
560                                    onexpunge(o)
561
562                        post(onexpunge_on_all)
563                        self.validate_before_close = False
564
565                        for window in get_workspace().subWindowList():
566                            if window.widget() == self:
567                                window.close()
568                else:
569                    def create_instance_getter(new_object):
570                        return lambda:new_object[0]
571
572                    for _o in new_object:
573                        self.emit(
574                            self.entity_created_signal,
575                            create_instance_getter(new_object)
576                        )
577                    self.validate_before_close = False
578                    from camelot.view.workspace import NoDesktopWorkspace
579                    workspace = get_workspace()
580                    if isinstance(workspace, (NoDesktopWorkspace,)):
581                        self.close()
582                    else:
583                        for window in get_workspace().subWindowList():
584                            if window.widget() == self:
585                                window.close()
586
587            def validateClose(self):
588                logger.debug(
589                    'validate before close : %s' %
590                    self.validate_before_close
591                )
592                if self.validate_before_close:
593                    self.form_view.widget_mapper.submit()
594                    logger.debug(
595                        'unflushed rows : %s' %
596                        str(model.hasUnflushedRows())
597                    )
598                    if model.hasUnflushedRows():
599                        def validate(): return validator.isValid(0)
600                        post(validate, self.showMessage)
601                        return False
602                    else:
603                        return True
604                return True
605
606            def closeEvent(self, event):
607                if self.validateClose():
608                    event.accept()
609                else:
610                    event.ignore()
611
612        form = NewForm(parent)
613        if hasattr(admin, 'form_size'):
614            form.setMinimumSize(admin.form_size[0], admin.form_size[1])
615        return form
616   
617    @model_function
618    def delete(self, entity_instance):
619        """Delete an entity instance"""
620        del entity_instance
621       
622    @model_function
623    def flush(self, entity_instance):
624        """Flush the pending changes of this entity instance to the backend"""
625        pass
626   
627    @model_function
628    def add(self, entity_instance):
629        """Add an entity instance as a managed entity instance"""
630        pass
631   
632    @model_function
633    def copy(self, entity_instance):
634        """Duplicate this entity instance"""
635        new_entity_instance = entity_instance.__class__()
636        return new_entity_instance
Note: See TracBrowser for help on using the browser.