Changes between Version 13 and Version 14 of TestManagerForTracPluginGenericClass


Ignore:
Timestamp:
Jul 28, 2015, 11:06:27 AM (9 years ago)
Author:
figaro
Comment:

Cosmetic changes

Legend:

Unmodified
Added
Removed
Modified
  • TestManagerForTracPluginGenericClass

    v13 v14  
    55= Test Manager for Trac - Generic Persistent Class Framework =
    66
    7 The [wiki:TestManagerForTracPlugin Test Manager plugin] is comprised of four plugins, one of which is a '''Generic Persistent Class framework''', allowing plugin programmers to easily build and manage persistent objects in the Trac database.
     7The [wiki:TestManagerForTracPlugin Test Manager plugin] comprises four plugins, one of which is a '''Generic Persistent Class framework''', allowing plugin programmers to easily build and manage persistent objects in the Trac database.
    88
    99Inheriting from the '''!AbstractVariableFieldsObject''' base class provided with this plugin is a matter of a few lines of code, and will provide your objects with:
    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
    1817The [wiki:TracGenericWorkflowPlugin Generic Workflow Engine] and the [wiki:TestManagerForTracPlugin Test Manager] plugins leverage this plugin for their data models.
    1918
     
    2120
    2221The basic building blocks of the framework are:
    23 
    2422 * The '''!AbstractVariableFieldsObject''' base class, providing most object features. Inheriting from this class is a matter of a few lines of code, and will provide your objects with the features outlined above.
    25 
    2623 * The '''IConcreteClassProvider''' interface, allowing your plugin to register your classes with the framework, participate in the framework's objects factory and provide custom security.
    27 
    2824 * The '''!GenericClassModelProvider''' component, providing the objects factory, managing concrete class providers and handling metadata about custom classes and their fields.
    29 
    3025 * The '''!GenericClassSystem''' component, providing the framework logic for displaying and updating custom properties in the Web user interface, and providing support for the Trac search box.
    31 
    3226 * The '''need_db_upgrade()''' and '''upgrade_db()''' utility methods, providing the ability for plugins to declaratively create and upgrade their database tables to match the custom classes provided.
    33 
    3427 * The '''IGenericObjectChangeListener''' inteface, an extension point interface for components that require notification when objects of any particular type are created, modified, or deleted.
    3528
    36 [[BR]]
    3729== !AbstractVariableFieldsObject - The data model ==
    3830
    39 The plugin provides a base python class, '''!AbstractVariableFieldsObject''', originally derived from the base Trac Ticket class, which brings a rich set of features to any custom class that should inherit from it.
     31The plugin provides a base Python class, '''!AbstractVariableFieldsObject''', originally derived from the base Trac Ticket class, which brings a rich set of features to any custom class that should inherit from it.
    4032
    4133It represents a persistent object which fields are declaratively specified.
     
    4638Features:
    4739 * Support for custom fields, specified in the trac.ini file with the same syntax as for custom Ticket fields. Custom field values are kept in a "<schema>_custom" table.
    48  
    4940 * Keeping track of all changes to any field, into a separate "<schema>_change" table.
    50 
    5141 * A set of callbacks to allow for subclasses to control and perform actions pre and post any operation pertaining the object's lifecycle. Subclasses may give or deny permission for any operation to be performed.
    52 
    5342 * Registering listeners, via the IGenericObjectChangeListener interface, for object creation, modification and deletion.
    54 
    5543 * Searching objects matching any set of valorized fields, (even non-key fields), applying the "dynamic record" pattern. See the method list_matching_objects.
    56 
    57  * Very well commented code and full of debug messages.
    58 
    59 [[BR]]
     44 * Well commented code and full of debug messages.
     45
    6046A set of special fields help subclasses to implement their logic:
    61 
    6247 * self.exists : always tells whether the object currently exists in the database.
    63 
    6448 * self.resource: points to a Resource, in the trac environment, corresponding to this object. This is used, for example, in the workflow implementation.
    65 
    6649 * self.fields: points to an array of dictionary objects describing name, label, type and other properties of all of this object's fields.
    67 
    6850 * self.metadata: points to a dictionary object describing further meta-data about this object.
    6951   
    70 [[BR]]
    71 Note: database tables for specific realms are supposed to already exist: this object does not create any tables.
     52'''Note''': database tables for specific realms are supposed to already exist: this object does not create any tables.
    7253See below the !GenericClassModelProvider to see how to make the framework create the required tables declaratively.
    7354
    74 [[BR]]
    7555== How to create a custom persistent object ==
    7656
    7757This section will guide you with step-by-step instructions on how to create a custom object, including declaring the object's class, providing the fields metadata, and specifying the DB tables.
    7858
    79 Note: The following examples are taken from the [wiki:TracGenericWorkflowPlugin TracGenericWorkflow plugin]. Refer to the corresponding code if you want more context.
    80 
    81 The following is the '''basic set of imports''' you will need to create a custom object. The first one is needed to let your class inherit from the basic abstract class, the others to define your concrete model provider.
    82 
    83 {{{
     59'''Note''': The following examples are taken from the [wiki:TracGenericWorkflowPlugin TracGenericWorkflow plugin]. Refer to the corresponding code if you want more context.
     60
     61The following is the '''basic set of imports''' you will need to create a custom object. The first one is needed to let your class inherit from the basic abstract class, the others to define your concrete model provider:
     62
     63{{{#!python
    8464from tracgenericclass.model import IConcreteClassProvider, AbstractVariableFieldsObject, AbstractWikiPageWrapper, need_db_create_for_realm, create_db_for_realm, need_db_upgrade_for_realm, upgrade_db_for_realm
    8565}}}
    8666
    87 [[BR]]
    88 Then we must '''declare the concrete class'''. See the description below the code box.
    89 
    90 {{{
     67Then we must '''declare the concrete class'''. See the description below the code box:
     68
     69{{{#!python
    9170class ResourceWorkflowState(AbstractVariableFieldsObject):
    9271    # Fields that have no default, and must not be modified directly by the user
     
    11897The other parameters you see in this example are just specific to the workflow object.
    11998
    120 Talking about the constructor we must see the different uses you can make of it.
    121 
    122 In fact, you can:
    123 
     99Talking about the constructor we must see the different uses you can make of it. In fact, you can:
    124100 * Create an empty object, and also try to fetch it from the database, if an object with a matching key is found.
    125101   To fetch an existing object from the database and modify or delete it:
     
    127103   2. modify any other property via the {{{obj['fieldname'] = value}}} syntax, including custom fields.
    128104      This syntax is the only one to keep track of the changes to any field.
    129    3. call the save_changes() or delete() method on the object.[[BR]]
     105   3. call the save_changes() or delete() method on the object.
    130106 * Create a new object to be later stored in the database. In this case, the steps required are the following:
    131107   1. specify a key at contruction time,
    132108   2. set any other property via the {{{obj['fieldname'] = value}}} syntax, including custom fields,
    133    3. call the insert() method on the object.[[BR]]
    134  * Create an empty, template object, used for pattern matching search. To do this, do not specify a key at initialization time.[[BR]]
     109   3. call the insert() method on the object.
     110 * Create an empty, template object, used for pattern matching search. To do this, do not specify a key at initialization time.
    135111
    136112In the body of the constructor, there a few important things you must do:
    137  1. Clear the {{{self.values}}} dictionary field, which holds all of the object's property values,
    138  2. Set the parameters received in the constructor into {{{self.values}}}. Use the syntax where you assign values directly to the {{{self.values}}} dictionary here, not the one like {{{self['fieldname'] = value}}},
    139  3. Build the object's key (by using the provided {{{self.build_key_object()}}} method, if you wish),
    140  4. Call the superclass constructor, passing as the third argument the unique type name that will identify objects of your custom class. Remember that this name must match the name of the database table providing persistence to your objects.
     113 1. Clear the {{{self.values}}} dictionary field, which holds all of the object's property values.
     114 1. Set the parameters received in the constructor into {{{self.values}}}. Use the syntax where you assign values directly to the {{{self.values}}} dictionary here, not the one like {{{self['fieldname'] = value}}}.
     115 1. Build the object's key (by using the provided {{{self.build_key_object()}}} method, if you wish).
     116 1. Call the superclass constructor, passing as the third argument the unique type name that will identify objects of your custom class. Remember that this name must match the name of the database table providing persistence to your objects.
    141117
    142118Two more methods are required to make a functional subclass.
     
    147123The {{{create_instance()}}} is a factory method used to create an instance of this custom class, given a key dictionary object.
    148124
    149 [[BR]]
    150 '''This is it!!! ''' Our new class is ready to be used, with all the features outlined at the top of this page.
    151 
    152 [[BR]][[BR]]
    153 Well, actually before being able to use our new class, we must:
     125Finally, we must:
    154126 * '''Define our concrete class provider''', implementing the '''IConcreteClassProvider''' interface, and
    155127 * '''Declare the data model''' to be created into the Trac DB. The database will be created (and updated) by the framework.
    156128
    157 As you will see, this is far from complex. See the description below the code box.
    158 
    159 {{{
     129As you will see, this is far from complex. See the description below the code box:
     130
     131{{{#!python
    160132class GenericWorkflowModelProvider(Component):
    161133    """
     
    264236}}}
    265237
    266 [[BR]]
    267238First of all, this should be a component, because it must implement Trac interfaces.
    268239The interfaces to implement are the following:
     
    272243Then comes the declaration of the database schema, class fields and class metadata, to be later returned in the corresponding methods of the interface.
    273244
    274 [[BR]]
     245That is it! Our new class is ready to be used, with all the features outlined at the top of this page.
     246
    275247=== Upgrading the Database ===
    276248
     
    285257These upgrade scripts must follow a specific '''naming convention''': '''db_<table name>_<version>'''.
    286258
    287 The following script form TestManager plugin, for example, is named "db_testcaseinplan_2", meaning that it will upgrade the "testcaseinplan" table up from version 1 to version 2.
    288 
    289 {{{
     259The following script form TestManager plugin, for example, is named "db_testcaseinplan_2", meaning that it will upgrade the "testcaseinplan" table up from version 1 to version 2:
     260
     261{{{#!python
    290262from trac.db import Table, Column, Index, DatabaseManager
    291263from tracgenericclass.util import *
     
    323295}}}
    324296
    325 [[BR]]
    326297In case you need to update a table's structure (e.g. add columns), you will:
    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
     305Following is the '''IConcreteClassProvider interface''' documentation, which is pretty explanatory about the format and meaning of the required data:
     306
     307{{{#!python
     308def get_realms():
     309    """
     310    Return class realms provided by the component.
     311
     312    :rtype: `basestring` generator
     313    """
     314
     315def 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
     335def 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       
     351def 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.
    389358               
    390                 Available metadata properties are:
    391                     label: A User-friendly name for the objects in this class.
    392                     searchable: If present and equal to True indicates the class
    393                                 partecipates in the Trac search framework, and
    394                                 must implement the get_search_results() method.
    395                     'has_custom': If present and equal to True indicates the class
    396                                   supports custom fields.
    397                     'has_change': If present and equal to True indicates the class
    398                                   supports property change history.
     359            Available metadata properties are:
     360                label: A User-friendly name for the objects in this class.
     361                searchable: If present and equal to True indicates the class
     362                            participates in the Trac search framework, and
     363                            must implement the get_search_results() method.
     364                'has_custom': If present and equal to True indicates the class
     365                              supports custom fields.
     366                'has_change': If present and equal to True indicates the class
     367                              supports property change history.
    399368                   
    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
     377def 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
     385def 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    """
    436405}}}
    437406
     
    440409 * Perform the database upgrade if required. This is achieved through the utility method {{{upgrade_db()}}}. '''Note:''' only creating all the tables is currently supported, not altering existing tables (for example due to your plugin database schema changing between versions).
    441410
    442 [[BR]]
    443411=== Keeping control of your objects ===
    444412
    445 We have seen how easy is to define a class and its properties, and below we will see how easy it is to then use these classes in the client code.
    446 
    447 The framework will handle all of the burden of fetching the database rows, populating the object, keeping track of the changes, saving it when you want to, and so on.
     413We have seen how to define a class and its properties, and below we will see how to then use these classes in the client code.
     414
     415The framework will handle the fetching the database rows, populating the object, keeping track of the changes, saving it when you want to, and so on.
    448416
    449417Anyway, there may be cases where you want to keep control on what's being done, and perhaps prevent some of these operations to occur, based on your own logic.
     
    453421 * The "post" methods get called after the corresponding operation has been performed, before the transaction is committed. They give you the opportunity to perform further custom work in the context of the same operation.
    454422
    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]]
     423The following is the list of such methods, from the !AbstractVariableFieldsObject class documentation:
     424
     425{{{#!python
     426def 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
     435def 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       
     442def 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
     451def 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       
     458def 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
     467def 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       
     474def 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
     483def 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       
     490def 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       
     499def 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       
     506def 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
    548515== A sample use ==
    549516
    550 Now that we have our new class and provider in place, '''let's see how to use them'''!
     517Now that we have our new class and provider in place, let's see how to use them!
    551518
    552519=== Creating a new, non-existing object ===
    553520
    554521To create a new, non existing object of our new class, we follow the steps outlined above:
    555 
    556    1. specify a key at contruction time,
    557    2. set any other property via the {{{obj['fieldname'] = value}}} syntax, including custom fields,
    558    3. call the insert() method on the object.
    559 
    560 See the following code.
    561 
    562 {{{
     522 1. specify a key at contruction time,
     523 1. set any other property via the {{{obj['fieldname'] = value}}} syntax, including custom fields,
     524 1. call the insert() method on the object.
     525
     526See the following code:
     527
     528{{{#!python
    563529# Let's first import our new class definition.
    564530# Note that you don't have to deal with the framework in any way, the class may be defined on its own.
     
    566532}}}
    567533
    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.
     539rws = ResourceWorkflowState(self.env, id, sometext)
     540   
     541# We can here check whether the object was found in the database
     542if rws.exists:
     543    # The object already exists! So we can get its property values
     544    print rws['state']
     545else:
     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()
    584550}}}
    585551
     
    589555
    590556We follow the steps outlined above:
    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
     561See the code below:
     562
     563{{{#!python
     564rws = ResourceWorkflowState(self.env, id, sometext)
     565   
     566if 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'])
    613577}}}
    614578
     
    618582=== Delete an object ===
    619583
    620 Deleting an object is as simple as calling the {{{delete()}}} method on the object instance.
    621 
    622 See the following code.
    623 
    624 {{{
    625     rws = ResourceWorkflowState(self.env, id, sometext)
    626    
    627     if rws.exists:
    628         # The object already exists.
    629         # Now we want to delete the object from the database. The following code will delete the
    630         # object and also call all of the registered listeners.
    631         try:
    632             rws.delete()
    633         except:
    634             self.log.info("Error deleting the resource with id %s" % rws['id'])
     584Deleting an object is as simple as calling the {{{delete()}}} method on the object instance:
     585
     586{{{#!python
     587rws = ResourceWorkflowState(self.env, id, sometext)
     588   
     589if rws.exists:
     590    # The object already exists.
     591    # Now we want to delete the object from the database. The following code will delete the
     592    # object and also call all of the registered listeners.
     593    try:
     594        rws.delete()
     595    except:
     596        self.log.info("Error deleting the resource with id %s" % rws['id'])
    635597}}}
    636598
     
    639601You can get a list of objects matching a particular set of properties - i.e. pattern matching - very easily:
    640602
    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
     607See the following code:
     608
     609{{{#!python
     610# We create a template object here, i.e. not providing a key.
     611rws_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
     615rws_search['state'] = 'new'
     616
     617# We then start the search
     618for 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']
    660622}}}
    661623
     
    670632See the following code:
    671633
    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
     635rws = ResourceWorkflowState(self.env, id, sometext)
     636   
     637if 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'])
    686648}}}
    687649
     
    690652A couple of utility methods in the base class allow clients to get or get multiple values at the same time.
    691653
    692 See the following code for details.
    693 
    694 {{{
    695     def get_values(self, prop_names):
    696         """
    697         Returns a list of the values for the specified properties,
    698         in the same order as the property names.
    699         """
     654See the following code:
     655
     656{{{#!python
     657def get_values(self, prop_names):
     658    """
     659    Returns a list of the values for the specified properties,
     660    in the same order as the property names.
     661    """
    700662               
    701     def set_values(self, props):
    702         """
    703         Sets multiple properties into this object.
    704         props must be a dictionary object with the names and values to set.       
    705        
    706         Note: this method does not keep history of property changes.
    707         """
    708 }}}
    709 
    710 [[BR]]
     663def set_values(self, props):
     664    """
     665    Sets multiple properties into this object.
     666    props must be a dictionary object with the names and values to set.       
     667       
     668    Note: this method does not keep history of property changes.
     669    """
     670}}}
     671
    711672== Trac Resource coupling ==
    712673
     
    715676TODO: Fill in this section.
    716677
    717 [[BR]]
    718678== Providing specific results to the Trac search engine ==
    719679
     
    726686For details about how to implement this method, refer to the base Trac documentation about the {{{ISearchSource}}} interface in the {{{trac.search}}} package.
    727687
    728 [[BR]]
    729688== IGenericObjectChangeListener - Registering listeners to objects lifecycle events ==
    730689
     
    733692To register a listener for any particular class type, i.e. "realm", your component must implement the {{{IGenericObjectChangeListener}}} from the {{{tracgenericclass.api}}} package.
    734693
    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]]
     694Following is the documentation from the interface itself:
     695
     696{{{#!python
     697def object_created(g_object):
     698    """Called when an object is created."""
     699
     700def 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
     707def object_deleted(g_object):
     708    """Called when an object is deleted."""
     709}}}
     710
    753711The object that has just been created, modified or deleted is passed along with the methods in the interface.
    754712
     
    756714
    757715 * The object's type can be retrieved from the {{{realm}}} field:
    758 {{{
     716 {{{#!python
    759717object_realm = g_object.realm
    760718}}}