10 | | * '''Persistence''', in the Trac database. The database schema will be created and updated automatically. |
11 | | * '''Custom Properties''', your Users will be able to specify in their trac.ini files. Also automatic Web user interface support is provided. |
12 | | * '''Change History tracking''', recording User, timestamp and values before and after of every object's property change. |
13 | | * '''Object lifecycle Listeners''', allowing your plugins or others to register for feedback on objects creation, deletion and modification. |
14 | | * '''Custom Authorization''', allowing custom permissions to control any access to your objects. |
15 | | * '''Integrated Search''', both via the Trac general Search box and programmatically, using pattern matching. |
16 | | |
17 | | [[BR]] |
| 10 | * '''Persistence''', in the Trac database. The database schema will be created and updated automatically. |
| 11 | * '''Custom Properties''', your Users will be able to specify in their trac.ini files. Also automatic Web user interface support is provided. |
| 12 | * '''Change History tracking''', recording User, timestamp and values before and after of every object's property change. |
| 13 | * '''Object lifecycle Listeners''', allowing your plugins or others to register for feedback on objects creation, deletion and modification. |
| 14 | * '''Custom Authorization''', allowing custom permissions to control any access to your objects. |
| 15 | * '''Integrated Search''', both via the Trac general Search box and programmatically, using pattern matching. |
| 16 | |
327 | | |
328 | | 1. Create a temporary table with the same structure as the old table |
329 | | 2. Copy all the contents of the current table into the temporary table |
330 | | 3. Drop the original table |
331 | | 4. Recreate the original table with the new structure |
332 | | 5. Copy back all the contents from the temporary table into the updated original table |
333 | | 6. Drop the temporary table |
334 | | |
335 | | [[BR]] |
336 | | Following is the '''IConcreteClassProvider interface''' documentation, which is pretty explanatory about the format and meaning of the required data. |
337 | | |
338 | | {{{ |
339 | | def get_realms(): |
340 | | """ |
341 | | Return class realms provided by the component. |
342 | | |
343 | | :rtype: `basestring` generator |
344 | | """ |
345 | | |
346 | | def get_data_models(): |
347 | | """ |
348 | | Return database tables metadata to allow the framework to create the |
349 | | db schema for the classes provided by the component. |
350 | | |
351 | | :rtype: a dictionary, which keys are schema names and values |
352 | | are dictionaries with table metadata, as in the following example: |
353 | | return {'sample_realm': |
354 | | {'table': |
355 | | Table('samplerealm', key = ('id', 'otherid'))[ |
356 | | Column('id'), |
357 | | Column('otherid'), |
358 | | Column('prop1'), |
359 | | Column('prop2'), |
360 | | Column('time', type='int64')], |
361 | | 'has_custom': True, |
362 | | 'has_change': True}, |
363 | | } |
364 | | """ |
365 | | |
366 | | def get_fields(): |
367 | | """ |
368 | | Return the standard fields for classes in all the realms |
369 | | provided by the component. |
370 | | |
371 | | :rtype: a dictionary, which keys are realm names and values |
372 | | are arrays of fields metadata, as in the following example: |
373 | | return {'sample_realm': [ |
374 | | {'name': 'id', 'type': 'text', 'label': N_('ID')}, |
375 | | {'name': 'otherid', 'type': 'text', 'label': N_('Other ID')}, |
376 | | {'name': 'prop1', 'type': 'text', 'label': N_('Property 1')}, |
377 | | {'name': 'prop2', 'type': 'text', 'label': N_('Property 2')}, |
378 | | {'name': 'time', 'type': 'time', 'label': N_('Last Change')} |
379 | | } |
380 | | """ |
381 | | |
382 | | def get_metadata(): |
383 | | """ |
384 | | Return a set of metadata about the classes in all the realms |
385 | | provided by the component. |
386 | | |
387 | | :rtype: a dictionary, which keys are realm names and values |
388 | | are dictionaries of properties. |
| 298 | 1. Create a temporary table with the same structure as the old table. |
| 299 | 1. Copy all the contents of the current table into the temporary table. |
| 300 | 1. Drop the original table. |
| 301 | 1. Recreate the original table with the new structure. |
| 302 | 1. Copy back all the contents from the temporary table into the updated original table. |
| 303 | 1. Drop the temporary table. |
| 304 | |
| 305 | Following is the '''IConcreteClassProvider interface''' documentation, which is pretty explanatory about the format and meaning of the required data: |
| 306 | |
| 307 | {{{#!python |
| 308 | def get_realms(): |
| 309 | """ |
| 310 | Return class realms provided by the component. |
| 311 | |
| 312 | :rtype: `basestring` generator |
| 313 | """ |
| 314 | |
| 315 | def get_data_models(): |
| 316 | """ |
| 317 | Return database tables metadata to allow the framework to create the |
| 318 | db schema for the classes provided by the component. |
| 319 | |
| 320 | :rtype: a dictionary, which keys are schema names and values |
| 321 | are dictionaries with table metadata, as in the following example: |
| 322 | return {'sample_realm': |
| 323 | {'table': |
| 324 | Table('samplerealm', key = ('id', 'otherid'))[ |
| 325 | Column('id'), |
| 326 | Column('otherid'), |
| 327 | Column('prop1'), |
| 328 | Column('prop2'), |
| 329 | Column('time', type='int64')], |
| 330 | 'has_custom': True, |
| 331 | 'has_change': True}, |
| 332 | } |
| 333 | """ |
| 334 | |
| 335 | def get_fields(): |
| 336 | """ |
| 337 | Return the standard fields for classes in all the realms |
| 338 | provided by the component. |
| 339 | |
| 340 | :rtype: a dictionary, which keys are realm names and values |
| 341 | are arrays of fields metadata, as in the following example: |
| 342 | return {'sample_realm': [ |
| 343 | {'name': 'id', 'type': 'text', 'label': N_('ID')}, |
| 344 | {'name': 'otherid', 'type': 'text', 'label': N_('Other ID')}, |
| 345 | {'name': 'prop1', 'type': 'text', 'label': N_('Property 1')}, |
| 346 | {'name': 'prop2', 'type': 'text', 'label': N_('Property 2')}, |
| 347 | {'name': 'time', 'type': 'time', 'label': N_('Last Change')} |
| 348 | } |
| 349 | """ |
| 350 | |
| 351 | def get_metadata(): |
| 352 | """ |
| 353 | Return a set of metadata about the classes in all the realms |
| 354 | provided by the component. |
| 355 | |
| 356 | :rtype: a dictionary, which keys are realm names and values |
| 357 | are dictionaries of properties. |
400 | | See the following example: |
401 | | return {'sample_realm': { |
402 | | 'label': "Sample Realm", |
403 | | 'searchable': True |
404 | | } |
405 | | } |
406 | | """ |
407 | | |
408 | | def create_instance(realm, props=None): |
409 | | """ |
410 | | Return an instance of the specified realm, with the specified properties, |
411 | | or an empty object if props is None. |
412 | | |
413 | | :rtype: `AbstractVariableFieldsObject` sub-class instance |
414 | | """ |
415 | | |
416 | | def check_permission(req, realm, key_str=None, operation='set', name=None, value=None): |
417 | | """ |
418 | | Checks whether the logged in User has permission to perform |
419 | | the specified operation on a resource of the specified realm and |
420 | | optionally with the specified key. |
421 | | |
422 | | Raise an exception if authorization is denied. |
423 | | |
424 | | Possible operations are: |
425 | | 'set': set a property with a value. 'name' and 'value' parameters are required. |
426 | | 'search': search for objects of this class. |
427 | | |
428 | | :param key_str: optional, the object's key, in the form of a string representing |
429 | | a dictionary. To get a dictionary back from this string, use the |
430 | | get_dictionary_from_string() function in the |
431 | | tracgenericclass.util package. |
432 | | :param operation: optional, the operation to be performed on the object. |
433 | | :param name: optional property name, valid for the 'set' operation type |
434 | | :param value: optional property value, valid for the 'set' operation type |
435 | | """ |
| 369 | See the following example: |
| 370 | return {'sample_realm': { |
| 371 | 'label': "Sample Realm", |
| 372 | 'searchable': True |
| 373 | } |
| 374 | } |
| 375 | """ |
| 376 | |
| 377 | def create_instance(realm, props=None): |
| 378 | """ |
| 379 | Return an instance of the specified realm, with the specified properties, |
| 380 | or an empty object if props is None. |
| 381 | |
| 382 | :rtype: `AbstractVariableFieldsObject` sub-class instance |
| 383 | """ |
| 384 | |
| 385 | def check_permission(req, realm, key_str=None, operation='set', name=None, value=None): |
| 386 | """ |
| 387 | Checks whether the logged in User has permission to perform |
| 388 | the specified operation on a resource of the specified realm and |
| 389 | optionally with the specified key. |
| 390 | |
| 391 | Raise an exception if authorization is denied. |
| 392 | |
| 393 | Possible operations are: |
| 394 | 'set': set a property with a value. 'name' and 'value' parameters are required. |
| 395 | 'search': search for objects of this class. |
| 396 | |
| 397 | :param key_str: optional, the object's key, in the form of a string representing |
| 398 | a dictionary. To get a dictionary back from this string, use the |
| 399 | get_dictionary_from_string() function in the |
| 400 | tracgenericclass.util package. |
| 401 | :param operation: optional, the operation to be performed on the object. |
| 402 | :param name: optional property name, valid for the 'set' operation type |
| 403 | :param value: optional property value, valid for the 'set' operation type |
| 404 | """ |
455 | | The following is the list of such methods, from the !AbstractVariableFieldsObject class documentation. |
456 | | |
457 | | {{{ |
458 | | def pre_fetch_object(self, db): |
459 | | """ |
460 | | Use this method to perform initialization before fetching the |
461 | | object from the database. |
462 | | Return False to prevent the object from being fetched from the |
463 | | database. |
464 | | """ |
465 | | return True |
466 | | |
467 | | def post_fetch_object(self, db): |
468 | | """ |
469 | | Use this method to further fulfill your object after being |
470 | | fetched from the database. |
471 | | """ |
472 | | pass |
473 | | |
474 | | def pre_insert(self, db): |
475 | | """ |
476 | | Use this method to perform work before inserting the |
477 | | object into the database. |
478 | | Return False to prevent the object from being inserted into the |
479 | | database. |
480 | | """ |
481 | | return True |
482 | | |
483 | | def post_insert(self, db): |
484 | | """ |
485 | | Use this method to perform further work after your object has |
486 | | been inserted into the database. |
487 | | """ |
488 | | pass |
489 | | |
490 | | def pre_save_changes(self, db): |
491 | | """ |
492 | | Use this method to perform work before saving the object changes |
493 | | into the database. |
494 | | Return False to prevent the object changes from being saved into |
495 | | the database. |
496 | | """ |
497 | | return True |
498 | | |
499 | | def post_save_changes(self, db): |
500 | | """ |
501 | | Use this method to perform further work after your object |
502 | | changes have been saved into the database. |
503 | | """ |
504 | | pass |
505 | | |
506 | | def pre_delete(self, db): |
507 | | """ |
508 | | Use this method to perform work before deleting the object from |
509 | | the database. |
510 | | Return False to prevent the object from being deleted from the |
511 | | database. |
512 | | """ |
513 | | return True |
514 | | |
515 | | def post_delete(self, db): |
516 | | """ |
517 | | Use this method to perform further work after your object |
518 | | has been deleted from the database. |
519 | | """ |
520 | | pass |
521 | | |
522 | | def pre_save_as(self, old_key, new_key, db): |
523 | | """ |
524 | | Use this method to perform work before saving the object with |
525 | | a different identity into the database. |
526 | | Return False to prevent the object from being saved into the |
527 | | database. |
528 | | """ |
529 | | return True |
530 | | |
531 | | def post_save_as(self, old_key, new_key, db): |
532 | | """ |
533 | | Use this method to perform further work after your object |
534 | | has been saved into the database. |
535 | | """ |
536 | | pass |
537 | | |
538 | | def pre_list_matching_objects(self, db): |
539 | | """ |
540 | | Use this method to perform work before finding matches in the |
541 | | database. |
542 | | Return False to prevent the search. |
543 | | """ |
544 | | return True |
545 | | }}} |
546 | | |
547 | | [[BR]] |
| 423 | The following is the list of such methods, from the !AbstractVariableFieldsObject class documentation: |
| 424 | |
| 425 | {{{#!python |
| 426 | def pre_fetch_object(self, db): |
| 427 | """ |
| 428 | Use this method to perform initialization before fetching the |
| 429 | object from the database. |
| 430 | Return False to prevent the object from being fetched from the |
| 431 | database. |
| 432 | """ |
| 433 | return True |
| 434 | |
| 435 | def post_fetch_object(self, db): |
| 436 | """ |
| 437 | Use this method to further fulfill your object after being |
| 438 | fetched from the database. |
| 439 | """ |
| 440 | pass |
| 441 | |
| 442 | def pre_insert(self, db): |
| 443 | """ |
| 444 | Use this method to perform work before inserting the |
| 445 | object into the database. |
| 446 | Return False to prevent the object from being inserted into the |
| 447 | database. |
| 448 | """ |
| 449 | return True |
| 450 | |
| 451 | def post_insert(self, db): |
| 452 | """ |
| 453 | Use this method to perform further work after your object has |
| 454 | been inserted into the database. |
| 455 | """ |
| 456 | pass |
| 457 | |
| 458 | def pre_save_changes(self, db): |
| 459 | """ |
| 460 | Use this method to perform work before saving the object changes |
| 461 | into the database. |
| 462 | Return False to prevent the object changes from being saved into |
| 463 | the database. |
| 464 | """ |
| 465 | return True |
| 466 | |
| 467 | def post_save_changes(self, db): |
| 468 | """ |
| 469 | Use this method to perform further work after your object |
| 470 | changes have been saved into the database. |
| 471 | """ |
| 472 | pass |
| 473 | |
| 474 | def pre_delete(self, db): |
| 475 | """ |
| 476 | Use this method to perform work before deleting the object from |
| 477 | the database. |
| 478 | Return False to prevent the object from being deleted from the |
| 479 | database. |
| 480 | """ |
| 481 | return True |
| 482 | |
| 483 | def post_delete(self, db): |
| 484 | """ |
| 485 | Use this method to perform further work after your object |
| 486 | has been deleted from the database. |
| 487 | """ |
| 488 | pass |
| 489 | |
| 490 | def pre_save_as(self, old_key, new_key, db): |
| 491 | """ |
| 492 | Use this method to perform work before saving the object with |
| 493 | a different identity into the database. |
| 494 | Return False to prevent the object from being saved into the |
| 495 | database. |
| 496 | """ |
| 497 | return True |
| 498 | |
| 499 | def post_save_as(self, old_key, new_key, db): |
| 500 | """ |
| 501 | Use this method to perform further work after your object |
| 502 | has been saved into the database. |
| 503 | """ |
| 504 | pass |
| 505 | |
| 506 | def pre_list_matching_objects(self, db): |
| 507 | """ |
| 508 | Use this method to perform work before finding matches in the |
| 509 | database. |
| 510 | Return False to prevent the search. |
| 511 | """ |
| 512 | return True |
| 513 | }}} |
| 514 | |
568 | | {{{ |
569 | | # The following statement will create an empty object with a specific key, and suddenly |
570 | | # try to fetch an object with the same key from the database. |
571 | | # If it is found, then the object's properties will be filled with the corresponding values |
572 | | # from the database, and the internal field "exists" set to True. |
573 | | rws = ResourceWorkflowState(self.env, id, sometext) |
574 | | |
575 | | # We can here check whether the object was found in the database |
576 | | if rws.exists: |
577 | | # The object already exists! So we can get its property values |
578 | | print rws['state'] |
579 | | else: |
580 | | # Here we decide to create the object. So we fill in some other properties and then call the "insert()" method. |
581 | | # The object will be stored into the database, and all registered listeners will be called. |
582 | | rws['state'] = 'new' |
583 | | rws.insert() |
| 534 | {{{#!python |
| 535 | # The following statement will create an empty object with a specific key, and suddenly |
| 536 | # try to fetch an object with the same key from the database. |
| 537 | # If it is found, then the object's properties will be filled with the corresponding values |
| 538 | # from the database, and the internal field "exists" set to True. |
| 539 | rws = ResourceWorkflowState(self.env, id, sometext) |
| 540 | |
| 541 | # We can here check whether the object was found in the database |
| 542 | if rws.exists: |
| 543 | # The object already exists! So we can get its property values |
| 544 | print rws['state'] |
| 545 | else: |
| 546 | # Here we decide to create the object. So we fill in some other properties and then call the "insert()" method. |
| 547 | # The object will be stored into the database, and all registered listeners will be called. |
| 548 | rws['state'] = 'new' |
| 549 | rws.insert() |
591 | | |
592 | | 1. specify a key at contruction time: the object will be filled with all of the values form the database, |
593 | | 2. modify any other property via the {{{obj['fieldname'] = value}}} syntax, including custom fields. |
594 | | This syntax is the only one to keep track of the changes to any field. |
595 | | 3. call the save_changes() method on the object.[[BR]] |
596 | | |
597 | | See the code below. |
598 | | |
599 | | {{{ |
600 | | rws = ResourceWorkflowState(self.env, id, sometext) |
601 | | |
602 | | if rws.exists: |
603 | | # The object already exists. |
604 | | # Now we also want to modify the object's 'state' property, and save the updated object. |
605 | | rws['state'] = 'ok' |
606 | | |
607 | | # The following code will modify the object in the database and also call all |
608 | | # of the registered listeners. |
609 | | try: |
610 | | rws.save_changes(author, "State changed") |
611 | | except: |
612 | | self.log.info("Error saving the resource with id %s" % rws['id']) |
| 557 | 1. Specify a key at contruction time: the object will be filled with all of the values form the database. |
| 558 | 1. Modify any other property via the {{{obj['fieldname'] = value}}} syntax, including custom fields. This syntax is the only one to keep track of the changes to any field. |
| 559 | 1. Call the save_changes() method on the object. |
| 560 | |
| 561 | See the code below: |
| 562 | |
| 563 | {{{#!python |
| 564 | rws = ResourceWorkflowState(self.env, id, sometext) |
| 565 | |
| 566 | if rws.exists: |
| 567 | # The object already exists. |
| 568 | # Now we also want to modify the object's 'state' property, and save the updated object. |
| 569 | rws['state'] = 'ok' |
| 570 | |
| 571 | # The following code will modify the object in the database and also call all |
| 572 | # of the registered listeners. |
| 573 | try: |
| 574 | rws.save_changes(author, "State changed") |
| 575 | except: |
| 576 | self.log.info("Error saving the resource with id %s" % rws['id']) |
641 | | 1. Create an empty, template object, without specifying a key, |
642 | | 2. Give values to any properties you want the objects in the database to match, |
643 | | 3. Call the list_matching_objects() method on the object. |
644 | | |
645 | | See the following code. |
646 | | |
647 | | {{{ |
648 | | # We create a template object here, i.e. not providing a key. |
649 | | rws_search = ResourceWorkflowState(self.env) |
650 | | |
651 | | # Let's say we want to list all the objects in the database having a 'state' of 'new'. |
652 | | # We set the desired property values in the template objects |
653 | | rws_search['state'] = 'new' |
654 | | |
655 | | # We then start the search |
656 | | for rws in rws_search.list_matching_objects(): |
657 | | # At every cycle, rws will hold another result matching the search pattern, in the |
658 | | # form of a full-fetched object of the ResourceWorkflowState type. |
659 | | print rws['id'] |
| 603 | 1. Create an empty, template object, without specifying a key. |
| 604 | 1. Give values to any properties you want the objects in the database to match. |
| 605 | 1. Call the list_matching_objects() method on the object. |
| 606 | |
| 607 | See the following code: |
| 608 | |
| 609 | {{{#!python |
| 610 | # We create a template object here, i.e. not providing a key. |
| 611 | rws_search = ResourceWorkflowState(self.env) |
| 612 | |
| 613 | # Let's say we want to list all the objects in the database having a 'state' of 'new'. |
| 614 | # We set the desired property values in the template objects |
| 615 | rws_search['state'] = 'new' |
| 616 | |
| 617 | # We then start the search |
| 618 | for rws in rws_search.list_matching_objects(): |
| 619 | # At every cycle, rws will hold another result matching the search pattern, in the |
| 620 | # form of a full-fetched object of the ResourceWorkflowState type. |
| 621 | print rws['id'] |
672 | | {{{ |
673 | | rws = ResourceWorkflowState(self.env, id, sometext) |
674 | | |
675 | | if rws.exists: |
676 | | # The object already exists. |
677 | | # Now we want to duplicate it, by giving the object a different key and saving it. |
678 | | # The following code will save the new object into the database and also call all |
679 | | # of the registered listeners. |
680 | | try: |
681 | | new_key = {'id': '123456', 'res_realm': rws['res_realm']} |
682 | | rws['state'] = 'new' |
683 | | rws.save_as(new_key) |
684 | | except: |
685 | | self.log.info("Error saving the resource with id %s" % new_key['id']) |
| 634 | {{{#!python |
| 635 | rws = ResourceWorkflowState(self.env, id, sometext) |
| 636 | |
| 637 | if rws.exists: |
| 638 | # The object already exists. |
| 639 | # Now we want to duplicate it, by giving the object a different key and saving it. |
| 640 | # The following code will save the new object into the database and also call all |
| 641 | # of the registered listeners. |
| 642 | try: |
| 643 | new_key = {'id': '123456', 'res_realm': rws['res_realm']} |
| 644 | rws['state'] = 'new' |
| 645 | rws.save_as(new_key) |
| 646 | except: |
| 647 | self.log.info("Error saving the resource with id %s" % new_key['id']) |
735 | | Following is the documentation from the interface itself. |
736 | | |
737 | | {{{ |
738 | | def object_created(g_object): |
739 | | """Called when an object is created.""" |
740 | | |
741 | | def object_changed(g_object, comment, author, old_values): |
742 | | """Called when an object is modified. |
743 | | |
744 | | `old_values` is a dictionary containing the previous values of the |
745 | | fields that have changed. |
746 | | """ |
747 | | |
748 | | def object_deleted(g_object): |
749 | | """Called when an object is deleted.""" |
750 | | }}} |
751 | | |
752 | | [[BR]] |
| 694 | Following is the documentation from the interface itself: |
| 695 | |
| 696 | {{{#!python |
| 697 | def object_created(g_object): |
| 698 | """Called when an object is created.""" |
| 699 | |
| 700 | def object_changed(g_object, comment, author, old_values): |
| 701 | """Called when an object is modified. |
| 702 | |
| 703 | `old_values` is a dictionary containing the previous values of the |
| 704 | fields that have changed. |
| 705 | """ |
| 706 | |
| 707 | def object_deleted(g_object): |
| 708 | """Called when an object is deleted.""" |
| 709 | }}} |
| 710 | |