| 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 | |
|---|
| 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 |
|---|
| 38 | |
|---|
| 39 | |
|---|
| 40 | class 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 |
|---|