Commits

seccanj committed ca69ce9

Removed old directory structure.

Comments (0)

Files changed (8)

trunk/setup.py

-from setuptools import setup
-
-setup(
-    name='TestManager',
-    version='1.2.1',
-    packages=['testmanager'],
-    package_data={'testmanager' : ['*.txt', 'templates/*.html', 'htdocs/js/*.js', 'htdocs/css/*.css', 'htdocs/images/*.*']},
-    author = 'Roberto Longobardi, Marco Cipriani',
-    author_email='seccanj@gmail.com',
-    license='BSD. See the file LICENSE.txt contained in the package.',
-    url='http://trac-hacks.org/wiki/TestManagerForTracPlugin',
-    download_url='https://sourceforge.net/projects/testman4trac/files/',
-    description='Test management plugin for Trac',
-    long_description='A Trac plugin to create Test Cases, organize them in catalogs and track their execution status and outcome.',
-    keywords='trac plugin test case management project quality assurance statistics stats charts charting graph',
-    entry_points = {'trac.plugins': ['testmanager = testmanager']},
-    dependency_links=['http://svn.edgewall.org/repos/genshi/trunk#egg=Genshi-dev'],
-    install_requires=['Genshi >= 0.5'],
-    )

trunk/testmanager/labels.py

-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2010 Roberto Longobardi, Marco Cipriani
-#
-
-LABELS = {
-    'main_tab_title': "Test Manager",
-    'select_cat_to_move': "Select the catalog into which to paste the Test Case and click on 'Move the copied Test Case here'. ",
-    'select_cat_to_move2': "Select the catalog (even this one) into which to paste the Test Case and click on 'Move the copied Test Case here'. ",
-    'new_catalog': "New Catalog:",
-    'add_catalog': "Add a Catalog",
-    'new_subcatalog': "New Sub-Catalog:",
-    'add_subcatalog': "Add a Sub-Catalog",
-    'move_here': "Move the copied Test Case here",
-    'move_tc_help_msg': "The Test Case has been copied. Now select the catalog into which to move the Test Case and click on 'Move the copied Test Case here'. ",
-    'cancel': "Cancel",
-    'move_tc_button': "Move the Test Case into another catalog",
-    'test_case': "Test Case",
-    'open_ticket_button': "Open a Ticket on this Test Case",
-    'add_tc_button': "Add a Test Case",
-    'new_tc_label': "New Test Case:",
-    'tc_list': "Test Catalogs List",
-    'tc_catalog': "Test Catalog",
-    'all_catalogs': "All Catalogs",
-    'filter_label': "Filter:",
-    'filter_help': "Type the test to search for, even more than one word. You can also filter on the test case status (untested, successful, failed).",
-    'expand_all': "Expand all",
-    'collapse_all': "Collapse all",
-    'SUCCESSFUL': "Successful",
-    'FAILED': "Failed",
-    'TO_BE_TESTED': "Untested",
-    'change_status_label': "Change the Status:",
-    'status_change_hist': "Status change history",
-    'open': "Open",
-    'timestamp': "Timestamp",
-    'author': "Author",
-    'status': "Status",
-    'new_plan_label': "New Test Plan:",
-    'add_test_plan_button': "Generate a new Test Plan",
-    'test_plan': "Test Plan: ",
-    'back_to_catalog': "Back to the Catalog",
-    'back_to_plan': "Back to the Test Plan",
-    'regenerate_plan_button': "Regenerate Test Plan",
-    'test_plan_list': "Available Test Plans",
-    'plan_name': "Plan Name",
-    'open_testplan_title': "Open Test Plan",
-    'duplicate_tc_button': "Duplicate the Test Case",
-    'edit_test_case_label': "Edit the Test Case",
-    'edit_label': "Edit",
-    'update_button': "Save"
-}

trunk/testmanager/labels_en.py

-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2010 Roberto Longobardi, Marco Cipriani
-#
-
-LABELS = {
-    'main_tab_title': "Test Manager",
-    'select_cat_to_move': "Select the catalog into which to paste the Test Case and click on 'Move the copied Test Case here'. ",
-    'select_cat_to_move2': "Select the catalog (even this one) into which to paste the Test Case and click on 'Move the copied Test Case here'. ",
-    'new_catalog': "New Catalog:",
-    'add_catalog': "Add a Catalog",
-    'new_subcatalog': "New Sub-Catalog:",
-    'add_subcatalog': "Add a Sub-Catalog",
-    'move_here': "Move the copied Test Case here",
-    'move_tc_help_msg': "The Test Case has been copied. Now select the catalog into which to move the Test Case and click on 'Move the copied Test Case here'. ",
-    'cancel': "Cancel",
-    'move_tc_button': "Move the Test Case into another catalog",
-    'test_case': "Test Case",
-    'open_ticket_button': "Open a Ticket on this Test Case",
-    'add_tc_button': "Add a Test Case",
-    'new_tc_label': "New Test Case:",
-    'tc_list': "Test Catalogs List",
-    'tc_catalog': "Test Catalog",
-    'all_catalogs': "All Catalogs",
-    'filter_label': "Filter:",
-    'filter_help': "Type the test to search for, even more than one word. You can also filter on the test case status (untested, successful, failed).",
-    'expand_all': "Expand all",
-    'collapse_all': "Collapse all",
-    'SUCCESSFUL': "Successful",
-    'FAILED': "Failed",
-    'TO_BE_TESTED': "Untested",
-    'change_status_label': "Change the Status:",
-    'status_change_hist': "Status change history",
-    'open': "Open",
-    'timestamp': "Timestamp",
-    'author': "Author",
-    'status': "Status",
-    'new_plan_label': "New Test Plan:",
-    'add_test_plan_button': "Generate a new Test Plan",
-    'test_plan': "Test Plan: ",
-    'back_to_catalog': "Back to the Catalog",
-    'back_to_plan': "Back to the Test Plan",
-    'regenerate_plan_button': "Regenerate Test Plan",
-    'test_plan_list': "Available Test Plans",
-    'plan_name': "Plan Name",
-    'open_testplan_title': "Open Test Plan",
-    'duplicate_tc_button': "Duplicate the Test Case",
-    'edit_test_case_label': "Edit the Test Case",
-    'edit_label': "Edit",
-    'update_button': "Save"
-}

trunk/testmanager/labels_it.py

-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2010 Roberto Longobardi, Marco Cipriani
-#
-
-LABELS = {
-    'main_tab_title': "Test Manager",
-    'select_cat_to_move': "Seleziona il catalogo dove incollare il Caso di Test copiato e clicca su 'Sposta il Caso di Test copiato qui'. ",
-    'select_cat_to_move2': "Seleziona il catalogo (anche questo stesso) dove incollare il Caso di Test copiato e clicca su 'Sposta il Caso di Test copiato qui'. ",
-    'new_catalog': "Nuovo Catalogo:",
-    'add_catalog': "Aggiungi un Catalogo",
-    'new_subcatalog': "Nuovo Sotto Catalogo:",
-    'add_subcatalog': "Aggiungi un Sotto Catalogo",
-    'move_here': "Sposta il Caso di Test copiato qui",
-    'move_tc_help_msg': "Il Caso di Test e' stato copiato. Adesso seleziona il catalogo dove spostarlo e clicca su 'Sposta il Caso di Test copiato qui'. ",
-    'cancel': "Annulla",
-    'move_tc_button': "Sposta il Caso di Test in altro catalogo",
-    'test_case': "Casi di Test",
-    'open_ticket_button': "Apri un Ticket su questo Caso di Test",
-    'add_tc_button': "Aggiungi un Caso di Test",
-    'new_tc_label': "Nuovo Caso di Test:",
-    'tc_list': "Lista dei Cataloghi di Test",
-    'tc_catalog': "Catalogo di Test",
-    'all_catalogs': "Tutti i Cataloghi",
-    'filter_label': "Filtro:",
-    'filter_help': "Inserire il testo da cercare, anche piu\' parole. Si puo\' filtrare anche per stato dei test cases (da testare, successful, fallito).",
-    'expand_all': "Espandi tutto",
-    'collapse_all': "Comprimi tutto",
-    'SUCCESSFUL': "Successful",
-    'FAILED': "Fallito",
-    'TO_BE_TESTED': "Da testare",
-    'change_status_label': "Cambia lo Stato:",
-    'status_change_hist': "Storia dei cambiamenti di stato",
-    'open': "Apri",
-    'timestamp': "Timestamp",
-    'author': "Autore",
-    'status': "Stato",
-    'new_plan_label': "Nuovo Piano di Test:",
-    'add_test_plan_button': "Genera un nuovo Piano di Test",
-    'test_plan': "Piano di Test: ",
-    'back_to_catalog': "Torna al Catalogo",
-    'back_to_plan': "Torna al Piano di Test",
-    'regenerate_plan_button': "Rigenera il Piano di Test",
-    'test_plan_list': "Piani di Test disponibili",
-    'plan_name': "Nome del Piano",
-    'open_testplan_title': "Apri il Piano di Test",
-    'duplicate_tc_button': "Duplica il Caso di Test",
-    'edit_test_case_label': "Modifica il Caso di Test",
-    'edit_label': "Modifica",
-    'update_button': "Salva"
-}

trunk/testmanager/model.py

-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2010 Roberto Longobardi, Marco Cipriani
-#
-
-import copy
-import re
-import sys
-import time
-import traceback
-
-from datetime import date, datetime
-
-from trac.attachment import Attachment
-from trac.core import *
-from trac.db import Table, Column, Index
-from trac.env import IEnvironmentSetupParticipant
-from trac.resource import Resource, ResourceNotFound
-from trac.util.datefmt import utc, utcmax
-from trac.util.text import empty, CRLF
-from trac.util.translation import _, N_, gettext
-from trac.wiki.api import WikiSystem
-from trac.wiki.model import WikiPage
-from trac.wiki.web_ui import WikiModule
-
-from testmanager.util import *
-
-
-__all__ = ['AbstractVariableFieldsObject', 'AbstractWikiPageWrapper', 'TestCatalog', 'TestCase', 'TestCaseInPlan', 'TestPlan', 'TestManagerModelProvider']
-
-
-class AbstractVariableFieldsObject(object):
-    """ 
-    An object which fields are declaratively specified.
-    
-    The specific object "type" is specified during construction
-    as the "realm" parameter.
-    This name must also correspond to the database table storing the
-    corresponding objects, and is used as the base name for the 
-    custom fields table and the change tracking table (see below).
-    
-    Features:
-        * Support for custom fields, specified in the trac.ini file
-          with the same syntax as for custom Ticket fields. Custom
-          fields are kept in a "<schema>_custom" table
-        * Keeping track of all changes to any field, into a separate
-          "<schema>_change" table
-        * A set of callbacks to allow for subclasses to control and 
-          perform actions pre and post any operation pertaining the 
-          object's lifecycle
-        * Registering listeners, via the ITestObjectChangeListener
-          interface, for object creation, modification and deletion.
-        * Searching objects matching any set of valorized fields,
-          (even non-key fields), applying the "dynamic record" pattern. 
-          See the method list_matching_objects.
-    
-    Notes on special fields:
-    
-        self.exists : always tells whether the object currently exists 
-                      in the database.
-                      
-        self.resource: points to a Resource, in the trac environment,
-                       corresponding to this object. This has no 
-                       further use, at the moment.
-                       
-        self.fields: points to an array of dictionary objects describing
-                     name, label, type and other properties of all of
-                     this object's fields.
-                     
-        self.metadata: points to a dictionary object describing 
-                       further meta-data about this object.
-    
-    Note: database tables for specific realms are supposed to already
-          exist, this object does not creates any tables.
-          See below the TestManagerModelProvider to see how to 
-          declaratively create the required tables.
-    """
-
-    def __init__(self, env, realm='variable_fields_obj', key=None, db=None):
-        """
-        Creates an empty object and also tries to fetches it from the 
-        database, if an object with a matching key is found.
-        
-        To create an empty, template object, do not specify a key.
-        
-        To create an object to be later stored in the database:
-           1) specify a key at contruction time
-           2) set any other property via the obj['fieldname'] = value
-              syntax, including custom fields
-           3) call the insert() method.
-           
-        To fetch an existing object from the database:
-           1) specify a key at contruction time: the object will be 
-            filled with all of the values form the database
-           2) 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
-           3) call the save_changes() method.
-        """
-        self.env = env
-
-        self.exists = False
-        
-        self.realm = realm
-        
-        tmmodelprovider = TestManagerModelProvider(self.env)
-        
-        self.fields = tmmodelprovider.get_fields(realm)
-        self.time_fields = [f['name'] for f in self.fields
-                            if f['type'] == 'time']
-
-        self.metadata = tmmodelprovider.get_metadata(realm)
-
-        if key is not None and len(key) > 0:
-            self.key = key
-            self.resource = Resource(realm, self.gey_key_string())
-        else:
-            self.resource = None
-            
-        if not key or not self._fetch_object(key, db):
-            self._init_defaults(db)
-            self.exists = False
-
-        self.env.log.debug("Exists: %s" % self.exists)
-        self.env.log.debug(self.values)
-        
-        self._old = {}
-
-    def _get_db(self, db):
-        return db or self.env.get_read_db()
-
-    def get_key_prop_names(self):
-        """Returns an array with the fields representing the identity
-           of this object. 
-           The specified fields are assumed being also part of the 
-           self.fields array.
-           The specified fields are also assumed to correspond to
-           columns with same name in the database table.
-        """
-        return ['id']
-        
-    def get_key_prop_values(self):
-        """Returns an array of values for the properties returned by
-        get_key_prop_names.
-        """
-        result = []
-
-        for f in self.get_key_prop_names():
-             result.append(self.values[f])
-             
-        return result
-
-    def get_resource_id(self):
-        """ Returns a string representation of the object's identity.
-            Used with the trac Resource API.
-        """
-        return [str(self.values[f])+'|' for f in self.get_key_prop_names()]
-        
-    def _init_defaults(self, db=None):
-        """ Initializes default values for a new object, based on
-            default values specified in the trac.ini file.
-        """
-        for field in self.fields:
-            default = None
-            if field['name'] in self.protected_fields:
-                # Ignore for new - only change through workflow
-                pass
-            elif not field.get('custom'):
-                default = self.env.config.get(realm,
-                                              'default_' + field['name'])
-            else:
-                default = field.get('value')
-                options = field.get('options')
-                if default and options and default not in options:
-                    try:
-                        default = options[int(default)]
-                    except (ValueError, IndexError):
-                        self.env.log.warning('Invalid default value "%s" '
-                                             'for custom field "%s"'
-                                             % (default, field['name']))
-            if default:
-                self.values.setdefault(field['name'], default)
-
-    def _fetch_object(self, key, db=None):
-        self.env.log.debug('>>> _fetch_object')
-    
-        if db is None:
-            db = self._get_db(db)
-        
-        if not self.pre_fetch_object(db):
-            return
-        
-        row = None
-
-        # Fetch the standard fields
-        std_fields = [f['name'] for f in self.fields
-                      if not f.get('custom')]
-        cursor = db.cursor()
-
-        sql_where = "WHERE 1=1"
-        for k in self.get_key_prop_names():
-            sql_where += " AND " + k + "=%%s" 
-
-        self.env.log.debug("Searching for %s: %s" % (self.realm, sql_where))
-        for k in self.get_key_prop_names():
-            self.env.log.debug("%s = %s" % (k, self[k]))
-        
-        cursor.execute(("SELECT %s FROM %s " + sql_where)
-                       % (','.join(std_fields), self.realm), self.get_key_prop_values())
-        row = cursor.fetchone()
-
-        if not row:
-            #raise ResourceNotFound(_('The specified object of type %(realm)s does not exist.', 
-            #                         realm=self.realm), _('Invalid object key'))
-            self.env.log.debug("Object NOT found.")
-            return False
-
-        self.env.log.debug("Object found.")
-            
-        self.key = self.build_key_object()
-        for i, field in enumerate(std_fields):
-            value = row[i]
-            if field in self.time_fields:
-                self.values[field] = from_any_timestamp(value)
-            elif value is None:
-                self.values[field] = empty
-            else:
-                self.values[field] = value
-
-        if self.metadata['has_custom']:
-            # Fetch custom fields if available
-            custom_fields = [f['name'] for f in self.fields if f.get('custom')]
-            cursor.execute(("SELECT name,value FROM %s_custom " + sql_where)
-                           % self.realm, self.get_key_prop_values())
-
-            for name, value in cursor:
-                if name in custom_fields:
-                    if value is None:
-                        self.values[name] = empty
-                    else:
-                        self.values[name] = value
-
-        self.post_fetch_object(db)
-        
-        self.exists = True
-
-        self.env.log.debug('<<< _fetch_object')
-        return True
-        
-    def build_key_object(self):
-        """ Builds and returns a dictionary object with the key properties,
-            as returned by get_key_prop_names.
-        """
-        key = None
-        for k in self.get_key_prop_names():
-            if (self.values[k] is not None):
-                if key is None:
-                    key = {}
-
-                key[k] = self.values[k]
-        
-        return key
-
-    def gey_key_string(self):
-        """ Returns a JSON string with the object key properties
-        """
-        return get_string_from_dictionary(self.key)
-
-    def get_values_as_string(self, props):
-        """ Returns a JSON string for the specified object properties
-        """
-        return get_string_from_dictionary(props, self.values)
-
-    def __getitem__(self, name):
-        """ Allows for using the syntax "obj['fieldname']" to access this
-            object's values.
-        """
-        return self.values.get(name)
-
-    def __setitem__(self, name, value):
-        """ Allows for using the syntax "obj['fieldname']" to access this
-            object's values.
-            Also logs object modifications so the table <realm>_change 
-            can be updated.
-        """
-        if name in self.values:
-            self.env.log.debug("Value before: %s" % self.values[name])
-            
-        if name in self.values and self.values[name] == value:
-            return
-        if name not in self._old: # Changed field
-            self.env.log.debug("Changing field value.")
-            self._old[name] = self.values.get(name)
-        elif self._old[name] == value: # Change of field reverted
-            del self._old[name]
-        if value:
-            if isinstance(value, list):
-                raise TracError(_("Multi-values fields not supported yet"))
-            field = [field for field in self.fields if field['name'] == name]
-            if field and field[0].get('type') != 'textarea':
-                value = value.strip()
-        self.values[name] = value
-        self.env.log.debug("Value after: %s" % self.values[name])
-
-    def get_value_or_default(self, name):
-        """Return the value of a field or the default value if it is undefined
-        """
-        try:
-            value = self.values[name]
-            if value is not empty:
-                return value
-            field = [field for field in self.fields if field['name'] == name]
-            if field:
-                return field[0].get('value', '')
-        except KeyError:
-            pass
-        
-    def populate(self, values):
-        """Populate the object with 'suitable' values from a dictionary"""
-        field_names = [f['name'] for f in self.fields]
-        for name in [name for name in values.keys() if name in field_names]:
-            self[name] = values.get(name, '')
-
-        # We have to do an extra trick to catch unchecked checkboxes
-        for name in [name for name in values.keys() if name[9:] in field_names
-                     and name.startswith('checkbox_')]:
-            if name[9:] not in values:
-                self[name[9:]] = '0'
-
-    def insert(self, when=None, db=None):
-        """
-        Add object to database.
-        
-        Parameters:
-            When: a datetime object to specify a creation date.
-        
-        The `db` argument is deprecated in favor of `with_transaction()`.
-        """
-        self.env.log.debug('>>> insert')
-
-        assert not self.exists, 'Cannot insert an existing ticket'
-
-        # Add a timestamp
-        if when is None:
-            when = datetime.now(utc)
-        self.values['time'] = self.values['changetime'] = when
-
-        # Perform type conversions
-        values = dict(self.values)
-        for field in self.time_fields:
-            if field in values:
-                values[field] = to_any_timestamp(values[field])
-        
-        # Insert record
-        std_fields = []
-        custom_fields = []
-        for f in self.fields:
-            fname = f['name']
-            if fname in self.values:
-                if f.get('custom'):
-                    custom_fields.append(fname)
-                else:
-                    std_fields.append(fname)
-
-        @self.env.with_transaction(db)
-        def do_insert(db):
-            if not self.pre_insert(db):
-                return
-            
-            cursor = db.cursor()
-            cursor.execute("INSERT INTO %s (%s) VALUES (%s)"
-                           % (self.realm,
-                              ','.join(std_fields),
-                              ','.join(['%s'] * len(std_fields))),
-                           [values[name] for name in std_fields])
-
-            # Insert custom fields
-            key_names = self.get_key_prop_names()
-            key_values = self.get_key_prop_values()
-            if custom_fields and len(custom_fields) > 0:
-                self.env.log.debug('  Inserting custom fields')
-                cursor.executemany("""
-                INSERT INTO %s_custom (%s,name,value) VALUES (%s,%%s,%%s)
-                """ 
-                % (self.realm, 
-                   ','.join(key_names),
-                   ','.join(['%s'] * len(key_names))),
-                [to_list((key_values, name, self[name])) for name in custom_fields])
-
-            self.post_insert(db)
-                
-        self.exists = True
-        self.resource = self.resource(id=self.get_resource_id())
-        self._old = {}
-
-        from testmanager.api import TestManagerSystem
-        for listener in TestManagerSystem(self.env).change_listeners:
-            listener.object_created(self.realm, self)
-
-        self.env.log.debug('<<< insert')
-        return self.key
-
-    def save_changes(self, author=None, comment=None, when=None, db=None, cnum=''):
-        """
-        Store object changes in the database. The object must already exist in
-        the database.  Returns False if there were no changes to save, True
-        otherwise.
-        
-        The `db` argument is deprecated in favor of `with_transaction()`.
-        """
-        self.env.log.debug('>>> save_changes')
-        assert self.exists, 'Cannot update a new object'
-
-        if not self._old and not comment:
-            return False # Not modified
-
-        if when is None:
-            when = datetime.now(utc)
-        when_ts = to_any_timestamp(when)
-
-        @self.env.with_transaction(db)
-        def do_save(db):
-            if not self.pre_save_changes(db):
-                return
-            
-            cursor = db.cursor()
-
-            # store fields
-            custom_fields = [f['name'] for f in self.fields if f.get('custom')]
-            
-            key_names = self.get_key_prop_names()
-            key_values = self.get_key_prop_values()
-            sql_where = '1=1'
-            for k in key_names:
-                sql_where += " AND " + k + "=%%s" 
-
-            for name in self._old.keys():
-                if name in custom_fields:
-                    cursor.execute(("""
-                        SELECT * FROM %s_custom 
-                        WHERE name=%%s AND 
-                        """ + sql_where) % self.realm, to_list((name, key_values)))
-                        
-                    if cursor.fetchone():
-                        cursor.execute(("""
-                            UPDATE %s_custom SET value=%%s
-                            WHERE name=%%s AND 
-                            """ + sql_where) % self.realm, to_list((self[name], name, key_values)))
-                    else:
-                        cursor.execute("""
-                            INSERT INTO %s_custom (%s,name,value) 
-                            VALUES (%s,%%s,%%s)
-                            """ 
-                            % (self.realm, 
-                            ','.join(key_names),
-                            ','.join(['%s'] * len(key_names))),
-                            to_list((key_values, name, self[name])))
-                else:
-                    cursor.execute(("""
-                        UPDATE %s SET %s=%%s WHERE 
-                        """ + sql_where) 
-                        % (self.realm, name),
-                        to_list((self[name], key_values)))
-                
-                if self.metadata['has_change']:
-                    cursor.execute(("""
-                        INSERT INTO %s_change
-                            (%s, time,author,field,oldvalue,newvalue)
-                        VALUES (%s, %%s, %%s, %%s, %%s, %%s)
-                        """
-                        % (self.realm, 
-                        ','.join(key_names),
-                        ','.join(['%s'] * len(key_names)))),
-                        to_list((key_values, when_ts, author, name, 
-                        self._old[name], self[name])))
-            
-            self.post_save_changes(db)
-
-        old_values = self._old
-        self._old = {}
-        self.values['changetime'] = when
-
-        from testmanager.api import TestManagerSystem
-        for listener in TestManagerSystem(self.env).change_listeners:
-            listener.object_changed(self.realm, self, comment, author, old_values)
-
-        self.env.log.debug('<<< save_changes')
-        return True
-
-    def delete(self, db=None):
-        """Delete the object. Also clears the change history and the
-           custom fields.
-        
-        The `db` argument is deprecated in favor of `with_transaction()`.
-        """
-
-        self.env.log.debug('>>> delete')
-
-        @self.env.with_transaction(db)
-        def do_delete(db):
-            if not self.pre_delete(db):
-                return
-                
-            #Attachment.delete_all(self.env, 'ticket', self.id, db)
-
-            cursor = db.cursor()
-
-            key_names = self.get_key_prop_names()
-            key_values = self.get_key_prop_values()
-
-            sql_where = 'WHERE 1=1'
-            for k in key_names:
-                sql_where += " AND " + k + "=%%s" 
-
-            self.env.log.debug("Deleting %s: %s" % (self.realm, sql_where))
-            for k in key_names:
-                self.env.log.debug("%s = %s" % (k, self[k]))
-                           
-            cursor.execute(("DELETE FROM %s " + sql_where)
-                % self.realm, key_values)
-
-            if self.metadata['has_change']:
-                cursor.execute(("DELETE FROM %s_change " + sql_where)
-                    % self.realm, key_values)
-            
-            if self.metadata['has_custom']:
-                cursor.execute(("DELETE FROM %s_custom " + sql_where) 
-                    % self.realm, key_values)
-
-            self.post_delete(db)
-                
-        from testmanager.api import TestManagerSystem
-        for listener in TestManagerSystem(self.env).change_listeners:
-            listener.object_deleted(self.realm, self)
-        
-        self.exists = False
-        self.env.log.debug('<<< delete')
-
-    def save_as(self, new_key, when=None, db=None):
-        """
-        Saves (a copy of) the object with different key.
-        The previous object is not deleted, so if needed it must be
-        deleted explicitly.
-        """
-        self.env.log.debug('>>> save_as')
-
-        old_key = self.key
-        if self.pre_save_as(old_key, new_key, db):
-            self.key = new_key
-        
-            # Copy values from key into corresponding self.values field
-            for f in self.get_key_prop_names():
-                 self.values[f] = new_key[f]
-
-            self.exists = False
-
-            # Create object with new key
-            self.insert(when, db)
-        
-            self.post_save_as(old_key, new_key, db)
-
-        self.env.log.debug('<<< save_as')
-        
-    def get_non_empty_prop_names(self):
-        """ Returns a list of names of the fields that are not None.
-        """
-        std_field_names = []
-        custom_field_names = []
-
-        for field in self.fields:
-            n = field.get('name')
-
-            if n in self.values and self.values[n] is not None:
-                if not field.get('custom'):
-                    std_field_names.append(n)
-                else:
-                    custom_field_names.append(n)
-                
-        return std_field_names, custom_field_names
-        
-    def get_values(self, prop_names):
-        """ 
-        Returns a list of the values for the specified properties,
-        in the same order as the property names.
-        """
-        result = []
-        
-        for n in prop_names:
-            result.append(self.values[n])
-                
-        return result
-                
-    def set_values(self, props):
-        """
-        Sets multiple properties into this object.
-        
-        Note: this method does not keep history of property changes.
-        """
-        for n in props:
-            self.values[n] = props[n]
-                
-    def _get_key_from_row(self, row):
-        """
-        Given a database row with the key properties, builds a 
-        dictionary with this object's key.
-        """
-        key = {}
-        
-        for i, f in enumerate(self.get_key_prop_names()):
-            key[f] = row[i]
-
-        return key
-        
-    def create_instance(self, key):
-        """ 
-        Subclasses should override this method to create an instance
-        of them with the specified key.
-        """
-        pass
-            
-    def list_matching_objects(self, db=None):
-        """
-        List the objects that match the current values of this object's
-        fields.
-        To use this method, first create an instance with no key, then
-        fill some of its fields with the values you want to find a 
-        match on, then call this method.
-        A collection of objects found in the database matching the 
-        fields you had provided values for will be returned.
-        
-        See list_testplans below for an example of its use.
-        
-        The `db` argument is deprecated in favor of `with_transaction()`.
-        """
-        self.env.log.debug('>>> list_matching_objects')
-        
-        if db is None:
-            db = self._get_db(db)
-
-        self.pre_list_matching_objects(db)
-
-        cursor = db.cursor()
-
-        non_empty_std_names, non_empty_custom_names = self.get_non_empty_prop_names()
-        
-        non_empty_std_values = self.get_values(non_empty_std_names)
-        non_empty_custom_values = self.get_values(non_empty_custom_names)
-
-        sql_where = '1=1'
-        for k in non_empty_std_names:
-            sql_where += " AND " + k + "=%%s" 
-        
-        cursor.execute(("SELECT %s FROM %s WHERE " + sql_where)
-                       % (','.join(self.get_key_prop_names()), self.realm), 
-                       non_empty_std_values)
-
-        for row in cursor:
-            key = self._get_key_from_row(row)
-            self.env.log.debug('<<< list_matching_objects - returning result')
-            yield self.create_instance(key)
-
-        # TODO: Support custom fields here.
-        
-        self.env.log.debug('<<< list_matching_objects')
-       
-    def get_search_results(self, req, terms, filters):
-        if False:
-            yield None
-
-    # Following is a set of callbacks allowing subclasses to perform
-    # actions around the operations that pertain the lifecycle of 
-    # this object.
-    
-    def pre_fetch_object(self, db):
-        """ 
-        Use this method to perform initialization before fetching the
-        object from the database.
-        Return False to prevent the object from being fetched from the 
-        database.
-        """
-        return True
-
-    def post_fetch_object(self, db):
-        """
-        Use this method to further fulfill your object after being
-        fetched from the database.
-        """
-        pass
-        
-    def pre_insert(self, db):
-        """ 
-        Use this method to perform work before inserting the
-        object into the database.
-        Return False to prevent the object from being inserted into the 
-        database.
-        """
-        return True
-
-    def post_insert(self, db):
-        """
-        Use this method to perform further work after your object has
-        been inserted into the database.
-        """
-        pass
-        
-    def pre_save_changes(self, db):
-        """ 
-        Use this method to perform work before saving the object changes
-        into the database.
-        Return False to prevent the object changes from being saved into 
-        the database.
-        """
-        return True
-
-    def post_save_changes(self, db):
-        """
-        Use this method to perform further work after your object 
-        changes have been saved into the database.
-        """
-        pass
-        
-    def pre_delete(self, db):
-        """ 
-        Use this method to perform work before deleting the object from 
-        the database.
-        Return False to prevent the object from being deleted from the 
-        database.
-        """
-        return True
-
-    def post_delete(self, db):
-        """
-        Use this method to perform further work after your object 
-        has been deleted from the database.
-        """
-        pass
-        
-    def pre_save_as(self, old_key, new_key, db):
-        """ 
-        Use this method to perform work before saving the object with
-        a different identity into the database.
-        Return False to prevent the object from being saved into the 
-        database.
-        """
-        return True
-        
-    def post_save_as(self, old_key, new_key, db):
-        """
-        Use this method to perform further work after your object 
-        has been saved into the database.
-        """
-        pass
-        
-    def pre_list_matching_objects(self, db):
-        """ 
-        Use this method to perform work before finding matches in the 
-        database.
-        Return False to prevent the search.
-        """
-        return True
-
-
-class AbstractWikiPageWrapper(AbstractVariableFieldsObject):
-    """
-    This subclass is a generic object that is based on a wiki page,
-    identified by the 'page_name' field.
-    The wiki page lifecycle is managed along with the normal object's
-    one.     
-    """
-    def __init__(self, env, realm='wiki_wrapper_obj', key=None, db=None):
-        AbstractVariableFieldsObject.__init__(self, env, realm, key, db)
-    
-    def post_fetch_object(self, db):
-        self.wikipage = WikiPage(self.env, self.values['page_name'])
-    
-    def delete(self, del_wiki_page=True, db=None):
-        """
-        Delete the object. Also deletes the Wiki page if so specified in the parameters.
-        
-        The `db` argument is deprecated in favor of `with_transaction()`.
-        """
-        
-        # The actual wiki page deletion is delayed until pre_delete.
-        self.del_wiki_page = del_wiki_page
-        
-        AbstractVariableFieldsObject.delete(self, db)
-        
-    def pre_insert(self, db):
-        """ 
-        Assuming the following fields have been given a value before this call:
-        text, author, remote_addr, values['page_name']
-        """
-        
-        wikipage = WikiPage(self.env, self.values['page_name'])
-        wikipage.text = self.text
-        wikipage.save(self.author, '', self.remote_addr)
-        
-        self.wikipage = wikipage
-        
-        return True
-
-    def pre_save_changes(self, db):
-        """ 
-        Assuming the following fields have been given a value before this call:
-        text, author, remote_addr, values['page_name']
-        """
-        
-        wikipage = WikiPage(self.env, self.values['page_name'])
-        wikipage.text = self.text
-        wikipage.save(self.author, '', self.remote_addr)
-    
-        self.wikipage = wikipage
-
-        return True
-
-    def pre_delete(self, db):
-        """ 
-        Assuming the following fields have been given a value before this call:
-        values['page_name']
-        """
-        
-        if self.del_wiki_page:
-            wikipage = WikiPage(self.env, self.values['page_name'])
-            wikipage.delete()
-            
-        self.wikipage = None
-        
-        return True
-
-
-    def get_search_results(self, req, terms, filters):
-        """
-        Currently delegates the search to the Wiki module. 
-        """
-        for result in WikiModule(self.env).get_search_results(req, terms, ('wiki',)):
-            yield result
-
-
-        
-class AbstractTestDescription(AbstractWikiPageWrapper):
-    """
-    A test description object based on a Wiki page.
-    Concrete subclasses are TestCatalog and TestCase.
-    
-    Uses a textual 'id' as key.
-    
-    Comprises a title and a description, currently embedded in the wiki
-    page respectively as the first line and the rest of the text.
-    The title is automatically wiki-formatted as a second-level title
-    (i.e. sorrounded by '==').
-    """
-    
-    # Fields that must not be modified directly by the user
-    protected_fields = ('id', 'page_name')
-
-    def __init__(self, env, realm='testdescription', id=None, page_name=None, title=None, description=None, db=None):
-    
-        self.env = env
-        
-        self.values = {}
-
-        self.values['id'] = id
-        self.values['page_name'] = page_name
-
-        self.title = title
-        self.description = description
-
-        self.env.log.debug('Title: %s' % self.title)
-        self.env.log.debug('Description: %s' % self.description)
-    
-        key = self.build_key_object()
-    
-        AbstractWikiPageWrapper.__init__(self, env, realm, key, db)
-
-    def post_fetch_object(self, db):
-        # Fetch the wiki page
-        AbstractWikiPageWrapper.post_fetch_object(self, db)
-
-        # Then parse it and derive title, description and author
-        self.title = get_page_title(self.wikipage.text)
-        self.description = get_page_description(self.wikipage.text)
-        self.author = self.wikipage.author
-
-        self.env.log.debug('Title: %s' % self.title)
-        self.env.log.debug('Description: %s' % self.description)
-
-    def pre_insert(self, db):
-        """ Assuming the following fields have been given a value before this call:
-            title, description, author, remote_addr 
-        """
-    
-        self.text = '== '+self.title+' ==' + CRLF + CRLF + self.description
-        AbstractWikiPageWrapper.pre_insert(self, db)
-
-        return True
-
-    def pre_save_changes(self, db):
-        """ Assuming the following fields have been given a value before this call:
-            title, description, author, remote_addr 
-        """
-    
-        self.text = '== '+self.title+' ==' + CRLF + CRLF + self.description
-        AbstractWikiPageWrapper.pre_save_changes(self, db)
-        
-        return True
-
-    
-class TestCatalog(AbstractTestDescription):
-    """
-    A container for test cases and sub-catalogs.
-    
-    Test catalogs are organized in a tree. Since wiki pages are instead
-    on a flat plane, we use a naming convention to flatten the tree into
-    page names. These are examples of wiki page names for a tree:
-        TC          --> root of the tree. This page is automatically 
-                        created at plugin installation time.
-        TC_TT0      --> test catalog at the first level. Note that 0 is
-                        the catalog ID, generated at creation time.
-        TC_TT0_TT34 --> sample sub-catalog, with ID '34', of the catalog 
-                        with ID '0'
-        TC_TT27     --> sample other test catalog at first level, with
-                        ID '27'
-                        
-        There is not limit to the depth of a test tree.
-                        
-        Test cases are contained in test catalogs, and are always
-        leaves of the tree:
-
-        TC_TT0_TT34_TC65 --> sample test case, with ID '65', contained 
-                             in sub-catalog '34'.
-                             Note that test case IDs are independent on 
-                             test catalog IDs.
-    """
-    def __init__(self, env, id=None, page_name=None, title=None, description=None, db=None):
-    
-        AbstractTestDescription.__init__(self, env, 'testcatalog', id, page_name, title, description, db)
-
-    def list_subcatalogs(self):
-        """
-        Returns a list of the sub catalogs of this catalog.
-        """
-        # TODO: Implement method
-        return ()
-        
-    def list_testcases(self):
-        """
-        Returns a list of the test cases in this catalog.
-        """
-        # TODO: Implement method
-        return ()
-
-    def list_testplans(self, db=None):
-        """
-        Returns a list of test plans for this catalog.
-        """
-
-        tp_search = TestPlan(self.env)
-        tp_search['catid'] = self.values['id']
-        
-        for tp in tp_search.list_matching_objects(db):
-            yield tp
-
-    def create_instance(self, key):
-        return TestCatalog(self.env, key['id'])
-        
-    
-class TestCase(AbstractTestDescription):
-    def __init__(self, env, id=None, page_name=None, title=None, description=None, db=None):
-    
-        AbstractTestDescription.__init__(self, env, 'testcase', id, page_name, title, description, db)
-
-    def get_enclosing_catalog(self):
-        """
-        Returns the catalog containing this test case.
-        """
-        page_name = self.values['page_name']
-        cat_id = page_name.rpartition('TT')[2].rpartition('_')[0]
-        cat_page = page_name.rpartition('_TC')[0]
-        
-        return TestCatalog(self.env, cat_id, cat_page)
-        
-    def create_instance(self, key):
-        return TestCase(self.env, key['id'])
-        
-    def move_to(self, tcat, db=None):
-        """ 
-        Moves the test case into a different catalog.
-        
-        Note: the test case keeps its ID, but the old wiki page is
-        deleted and a new page is created with the new "path".
-        This means the page change history is lost.
-        """
-        
-        text = self.wikipage.text
-        
-        old_cat = self.get_enclosing_catalog()
-        
-        # Create new wiki page to store the test case
-        new_page_name = tcat['page_name'] + '_TC' + self['id']
-        new_page = WikiPage(self.env, new_page_name)
-               
-        new_page.text = text
-        new_page.save(self.author, "Moved from catalog \"%s\" (%s)" % (old_cat.title, old_cat['page_name']), '127.0.0.1')
-
-        # Remove test case from all the plans
-        tcip_search = TestCaseInPlan(self.env)
-        tcip_search['id'] = self.values['id']
-        for tcip in tcip_search.list_matching_objects(db):
-            tcip.delete(db)
-
-        # Delete old wiki page
-        self.wikipage.delete()
-
-        self['page_name'] = new_page_name
-        self.wikipage = new_page
-        
-        
-class TestCaseInPlan(AbstractVariableFieldsObject):
-    """
-    This object represents a test case in a test plan.
-    It keeps the latest test execution status (aka verdict).
-    
-    The status, as far as this class is concerned, can be just any 
-    string.
-    The plugin logic, anyway, currently recognizes only three hardcoded
-    statuses, but this can be evolved without need to modify also this
-    class. 
-    
-    The history of test execution status changes is instead currently
-    kept in another table, testcasehistory, which is not backed by any
-    python class. 
-    This is a duplication, since the 'changes' table also keeps track
-    of status changes, so the testcasehistory table may be removed in 
-    the future.
-    """
-    
-    # Fields that must not be modified directly by the user
-    protected_fields = ('id', 'planid', 'page_name', 'status')
-
-    def __init__(self, env, id=None, planid=None, page_name=None, status=None, db=None):
-        """
-        The test case in plan is related to a test case, the 'id' and 
-        'page_name' arguments, and to a test plan, the 'planid' 
-        argument.
-        """
-        self.values = {}
-
-        self.values['id'] = id
-        self.values['planid'] = planid
-        self.values['page_name'] = page_name
-        self.values['status'] = status
-
-        key = self.build_key_object()
-    
-        AbstractVariableFieldsObject.__init__(self, env, 'testcaseinplan', key, db)
-
-    def get_key_prop_names(self):
-        return ['id', 'planid']
-        
-    def create_instance(self, key):
-        return TestCaseInPlan(self.env, key['id'], key['planid'])
-        
-    def set_status(self, status, author, db=None):
-        """
-        Sets the execution status of the test case in the test plan.
-        This method immediately writes into the test case history, but
-        does not write the new status into the database table for this
-        test case in plan.
-        You need to call 'save_changes' to achieve that.
-        """
-        self['status'] = status
-
-        @self.env.with_transaction(db)
-        def do_set_status(db):
-            cursor = db.cursor()
-            sql = 'INSERT INTO testcasehistory (id, planid, time, author, status) VALUES (%s, %s, %s, %s, %s)'
-            cursor.execute(sql, (self.values['id'], self.values['planid'], to_any_timestamp(datetime.now(utc)), author, status))
-
-    def list_history(self, db=None):
-        """
-        Returns an ordered list of status changes, along with timestamp
-        and author, starting from the most recent.
-        """
-        if db is None:
-            db = self._get_db(db)
-        
-        cursor = db.cursor()
-
-        sql = "SELECT time, author, status FROM testcasehistory WHERE id=%s AND planid=%s ORDER BY time DESC"
-        
-        cursor.execute(sql, (self.values['id'], self.values['planid']))
-        for ts, author, status in cursor:
-            yield ts, author, status
-
-    
-class TestPlan(AbstractVariableFieldsObject):
-    """
-    A test plan represents a particular instance of test execution
-    for a test catalog.
-    You can create any number of test plans on any test catalog (or 
-    sub-catalog).
-    A test plan is associated to a test catalog, and to every 
-    test case in it, with the initial state equivalent to 
-    "to be executed".
-    The association with test cases is achieved through the 
-    TestCaseInPlan objects.
-    
-    For optimization purposes, a TestCaseInPlan is created in the
-    database only as soon as its status is changed (i.e. from "to be
-    executed" to something else).
-    So you cannot always count on the fact that a TestCaseInPlan 
-    actually exists for every test case in a catalog, when a particular
-    test plan has been created for it.
-    """
-    
-    # Fields that must not be modified directly by the user
-    protected_fields = ('id', 'catid', 'page_name', 'name', 'author', 'time')
-
-    def __init__(self, env, id=None, catid=None, page_name=None, name=None, author=None, db=None):
-        """
-        A test plan has an ID, generated at creation time and 
-        independent on those for test catalogs and test cases.
-        It is associated to a test catalog, the 'catid' and 'page_name'
-        arguments.
-        It has a name and an author.
-        """
-        self.values = {}
-
-        self.values['id'] = id
-        self.values['catid'] = catid
-        self.values['page_name'] = page_name
-        self.values['name'] = name
-        self.values['author'] = author
-
-        key = self.build_key_object()
-    
-        AbstractVariableFieldsObject.__init__(self, env, 'testplan', key, db)
-
-    def create_instance(self, key):
-        return TestPlan(self.env, key['id'])
-
-    
-        
-def simplify_whitespace(name):
-    """Strip spaces and remove duplicate spaces within names"""
-    if name:
-        return ' '.join(name.split())
-    return name
-        
-
-class TestManagerModelProvider(Component):
-    """
-    This class provides the data model for the test management plugin.
-    
-    The actual data model on the db is created starting from the
-    SCHEMA declaration below.
-    For each table, we specify whether to create also a '_custom' and
-    a '_change' table.
-    
-    This class also provides the specification of the available fields
-    for each class, being them standard fields and the custom fields
-    specified in the trac.ini file.
-    The custom field specification follows the same syntax as for
-    Tickets.
-    Currently, only 'text' type of fields are supported.
-    """
-
-    implements(IEnvironmentSetupParticipant)
-
-    SCHEMA = {'testconfig':
-                {'table':
-                    Table('testconfig', key = ('propname'))[
-                      Column('propname'),
-                      Column('value')],
-                 'has_custom': False,
-                 'has_change': False},
-              'testcatalog':  
-                {'table':
-                    Table('testcatalog', key = ('id'))[
-                          Column('id'),
-                          Column('page_name')],
-                 'has_custom': True,
-                 'has_change': True},
-              'testcase':  
-                {'table':
-                    Table('testcase', key = ('id'))[
-                          Column('id'),
-                          Column('page_name')],
-                 'has_custom': True,
-                 'has_change': True},
-              'testcaseinplan':  
-                {'table':
-                    Table('testcaseinplan', key = ('id', 'planid'))[
-                          Column('id'),
-                          Column('planid'),
-                          Column('page_name'),
-                          Column('status')],
-                 'has_custom': True,
-                 'has_change': True},
-              'testcasehistory':  
-                {'table':
-                    Table('testcasehistory', key = ('id', 'planid', 'time'))[
-                          Column('id'),
-                          Column('planid'),
-                          Column('time', type='int64'),
-                          Column('author'),
-                          Column('status'),
-                          Index(['id', 'planid', 'time'])],
-                 'has_custom': False,
-                 'has_change': False},
-              'testplan':  
-                {'table':
-                    Table('testplan', key = ('id'))[
-                          Column('id'),
-                          Column('catid'),
-                          Column('page_name'),
-                          Column('name'),
-                          Column('author'),
-                          Column('time', type='int64'),
-                          Index(['id']),
-                          Index(['catid'])],
-                 'has_custom': True,
-                 'has_change': True},
-              'resourceworkflowstate':  
-                {'table':
-                    Table('resourceworkflowstate', key = ('id', 'res_realm'))[
-                          Column('id'),
-                          Column('res_realm'),
-                          Column('state')],
-                 'has_custom': True,
-                 'has_change': True},
-            }
-
-
-    # Factory method
-    def get_object(self, realm, key):
-        obj = None
-        
-        if realm == 'testcatalog':
-            obj = TestCatalog(self.env, key['id'])
-        elif realm == 'testcase':
-            obj = TestCase(self.env, key['id'])
-        elif realm == 'testcaseinplan':
-            obj = TestCaseInPlan(self.env, key['id'], key['planid'])
-        elif realm == 'testplan':
-            obj = TestPlan(self.env, key['id'])
-        elif realm == 'resourceworkflowstate':
-            from testmanager.workflow import ResourceWorkflowState
-            obj = ResourceWorkflowState(self.env, key['id'], key['res_realm'])
-        
-        return obj
-        
-
-    # IEnvironmentSetupParticipant methods
-    def environment_created(self):
-        self._create_db(self.env.get_db_cnx())
-
-    def environment_needs_upgrade(self, db):
-        if self._need_initialization(db):
-            return True
-
-        return False
-
-    def upgrade_environment(self, db):
-        # Create db
-        if self._need_initialization(db):
-            self._upgrade_db(db)
-
-    def _need_initialization(self, db):
-        cursor = db.cursor()
-        try:
-            cursor.execute("select count(*) from testconfig")
-            cursor.fetchone()
-            cursor.execute("select count(*) from testcatalog")
-            cursor.fetchone()
-            cursor.execute("select count(*) from testcase")
-            cursor.fetchone()
-            cursor.execute("select count(*) from testcaseinplan")
-            cursor.fetchone()
-            cursor.execute("select count(*) from testcasehistory")
-            cursor.fetchone()
-            cursor.execute("select count(*) from testplan")
-            cursor.fetchone()
-            cursor.execute("select count(*) from resourceworkflowstate")
-            cursor.fetchone()
-            
-            return False
-        except:
-            db.rollback()
-            print("Testmanager needs to create the db")
-            return True
-        
-    def _create_db(self, db):
-        self._upgrade_db(db)
-        
-    def _upgrade_db(self, db):
-        try:
-            try:
-                from trac.db import DatabaseManager
-                db_backend, _ = DatabaseManager(self.env)._get_connector()
-            except ImportError:
-                db_backend = self.env.get_db_cnx()
-
-            self.env.log.debug("Upgrading DB...")
-                
-            # Create the required tables
-            cursor = db.cursor()
-            for realm in self.SCHEMA:
-                table_metadata = self.SCHEMA[realm]
-                tablem = table_metadata['table']
-
-                tname = tablem.name
-                key_names = [k for k in tablem.key]
-                
-               # Create base table
-                self.env.log.debug("Creating base table %s..." % tname)
-                for stmt in db_backend.to_sql(tablem):
-                    self.env.log.debug(stmt)
-                    cursor.execute(stmt)
-  
-                # Create custom fields table if required
-                if table_metadata['has_custom']:
-                    cols = []
-                    for k in key_names:
-                        # Determine type of column k
-                        type = 'text'
-                        for c in tablem.columns:
-                            if c.name == k:
-                                type = c.type
-                                
-                        cols.append(Column(k, type=type))
-                        
-                    cols.append(Column('name'))
-                    cols.append(Column('value'))
-                    
-                    custom_key = key_names
-                    custom_key.append('name')
-                    
-                    table_custom = Table(tname+'_custom', key = custom_key)[cols]
-                    self.env.log.debug("Creating custom table %s..." % table_custom.name)
-                    for stmt in db_backend.to_sql(table_custom):
-                        self.env.log.debug(stmt)
-                        cursor.execute(stmt)
-
-                # Create change history table if required
-                if table_metadata['has_change']:
-                    cols = []
-                    for k in key_names:
-                        # Determine type of column k
-                        type = 'text'
-                        for c in tablem.columns:
-                            if c.name == k:
-                                type = c.type
-
-                        cols.append(Column(k, type=type))
-                        
-                    cols.append(Column('time', type='int64'))
-                    cols.append(Column('author'))
-                    cols.append(Column('field'))
-                    cols.append(Column('oldvalue'))
-                    cols.append(Column('newvalue'))
-                    cols.append(Index(key_names))
-
-                    change_key = key_names
-                    change_key.append('time')
-                    change_key.append('field')
-
-                    table_change = Table(tname+'_change', key = change_key)[cols]
-                    self.env.log.debug("Creating change history table %s..." % table_change.name)
-                    for stmt in db_backend.to_sql(table_change):
-                        self.env.log.debug(stmt)
-                        cursor.execute(stmt)
-
-            # Create default values for configuration properties and initialize counters
-            cursor.execute("INSERT INTO testconfig (propname, value) VALUES ('NEXT_CATALOG_ID', '0')")
-            cursor.execute("INSERT INTO testconfig (propname, value) VALUES ('NEXT_TESTCASE_ID', '0')")
-            cursor.execute("INSERT INTO testconfig (propname, value) VALUES ('NEXT_PLAN_ID', '0')")
-            db.commit()
-
-            # Create the basic "TC" Wiki page, used as the root test catalog
-            tc_page = WikiPage(self.env, 'TC')
-            tc_page.text = ' '
-            tc_page.save('System', '', '127.0.0.1')
-
-        except:
-            db.rollback()
-            self.env.log.debug("Esxception during upgrade")
-            raise
-
-            
-    # Field management
-    all_fields = {}
-    all_custom_fields = {}
-    all_metadata = {}
-    
-    def reset_fields(self):
-        """Invalidate field cache."""
-        self.all_fields = {}
-        
-    def get_fields(self, realm):
-        self.env.log.debug(">>> get_fields")
-        
-        fields = copy.deepcopy(self.fields()[realm])
-        #label = 'label' # workaround gettext extraction bug
-        #for f in fields:
-        #    f[label] = gettext(f[label])
-
-        self.env.log.debug("<<< get_fields")
-        return fields
-        
-    def get_metadata(self, realm):
-        self.env.log.debug(">>> get_metadata")
-        
-        metadata = copy.deepcopy(self.metadata()[realm])
-
-        self.env.log.debug("<<< get_metadata")
-        return metadata
-        
-    def get_ticket_field_labels(self):
-        """Produce a (name,label) mapping from `get_fields`."""
-        return dict((f['name'], f['label']) for f in
-                    TestManagerModelProvider(self.env).get_fields())
-
-    def fields(self):
-        """Return the list of fields available for every realm."""
-
-        self.env.log.debug(">>> fields")
-
-        if not self.all_fields:
-            fields = {}
-            
-            # testcatalog
-            realm = 'testcatalog'
-            tmp_fields = []
-            tmp_fields.append({'name': 'id', 'type': 'text',
-                           'label': N_('ID')})
-            tmp_fields.append({'name': 'page_name', 'type': 'text',
-                           'label': N_('Wiki page name')})
-            self.append_custom_fields(tmp_fields, self.get_custom_fields_for_realm(realm))
-            fields[realm] = tmp_fields
-
-            # testcase
-            realm = 'testcase'
-            tmp_fields = []
-            tmp_fields.append({'name': 'id', 'type': 'text',
-                           'label': N_('ID')})
-            tmp_fields.append({'name': 'page_name', 'type': 'text',
-                           'label': N_('Wiki page name')})
-            self.append_custom_fields(tmp_fields, self.get_custom_fields_for_realm(realm))
-            fields[realm] = tmp_fields
-
-            # testcaseinplan
-            realm = 'testcaseinplan'
-            tmp_fields = []
-            tmp_fields.append({'name': 'id', 'type': 'text',
-                           'label': N_('ID')})
-            tmp_fields.append({'name': 'planid', 'type': 'text',
-                           'label': N_('Plan ID')})
-            tmp_fields.append({'name': 'page_name', 'type': 'text',
-                           'label': N_('Wiki page name')})
-            tmp_fields.append({'name': 'status', 'type': 'text',
-                           'label': N_('Status')})
-            self.append_custom_fields(tmp_fields, self.get_custom_fields_for_realm(realm))
-            fields[realm] = tmp_fields
-
-            # testplan
-            realm = 'testplan'
-            tmp_fields = []
-            tmp_fields.append({'name': 'id', 'type': 'text',
-                           'label': N_('ID')})
-            tmp_fields.append({'name': 'catid', 'type': 'text',
-                           'label': N_('Catalog ID')})
-            tmp_fields.append({'name': 'page_name', 'type': 'text',
-                           'label': N_('Wiki page name')})
-            tmp_fields.append({'name': 'name', 'type': 'text',
-                           'label': N_('Name')})
-            tmp_fields.append({'name': 'author', 'type': 'text',
-                           'label': N_('Author')})
-            tmp_fields.append({'name': 'time', 'type': 'time',
-                           'label': N_('Created')})
-            self.append_custom_fields(tmp_fields, self.get_custom_fields_for_realm(realm))
-            fields[realm] = tmp_fields
-
-            # resourceworkflowstate
-            realm = 'resourceworkflowstate'
-            tmp_fields = []
-            tmp_fields.append({'name': 'id', 'type': 'text',
-                           'label': N_('ID')})
-            tmp_fields.append({'name': 'res_realm', 'type': 'text',
-                           'label': N_('Resource realm')})
-            tmp_fields.append({'name': 'state', 'type': 'text',
-                           'label': N_('Workflow state')})
-            self.append_custom_fields(tmp_fields, self.get_custom_fields_for_realm(realm))
-            fields[realm] = tmp_fields
-
-
-            self.all_fields = fields
-
-            for r in self.all_fields:
-                self.env.log.debug("Fields for realm %s:" % r)
-                for f in self.all_fields[r]:
-                    self.env.log.debug("   %s : %s" % (f['name'], f['type']))
-                    if 'custom' in f:
-                        self.env.log.debug("     (custom)")
-
-        self.env.log.debug("<<< fields")
-
-        return self.all_fields
-
-    def metadata(self):
-        """Return the metadata available for every realm."""
-
-        self.env.log.debug(">>> metadata")
-
-        if not self.all_metadata:
-            metadata = {}
-            
-            # testcatalog
-            realm = 'testcatalog'
-            metadata[realm] = self._get_object_metadata(realm)
-
-            # testcase
-            realm = 'testcase'
-            metadata[realm] = self._get_object_metadata(realm)
-
-            # testcaseinplan
-            realm = 'testcaseinplan'
-            metadata[realm] = self._get_object_metadata(realm)
-
-            # testplan
-            realm = 'testplan'
-            metadata[realm] = self._get_object_metadata(realm)
-
-            # resourceworkflowstate
-            realm = 'resourceworkflowstate'
-            metadata[realm] = self._get_object_metadata(realm)
-
-            self.all_metadata = metadata
-
-        self.env.log.debug("<<< metadata")
-
-        return self.all_metadata
-
-    def append_custom_fields(self, fields, custom_fields):
-        if len(custom_fields) > 0:
-            for f in custom_fields:
-                fields.append(f)
-        
-    def get_custom_fields_for_realm(self, realm):
-        fields = []
-    
-        for field in self.get_custom_fields(realm):
-            field['custom'] = True
-            fields.append(field)
-            
-        return fields
-
-    def get_custom_fields(self, realm):
-        return copy.deepcopy(self.custom_fields(realm))
-
-    def custom_fields(self, realm):
-        """Return the list of available custom fields."""
-        
-        self.env.log.debug(">>> custom_fields")
-        
-        if not realm in self.all_custom_fields:
-            fields = []
-            config = self.config[realm+'-tm_custom']
-
-            self.env.log.debug(config.options())
-    
-            for name in [option for option, value in config.options()
-                         if '.' not in option]:
-                if not re.match('^[a-zA-Z][a-zA-Z0-9_]+$', name):
-                    self.log.warning('Invalid name for custom field: "%s" '
-                                     '(ignoring)', name)
-                    continue
-
-                self.env.log.debug("  Option: %s" % name)
-                         
-                field = {
-                    'name': name,
-                    'type': config.get(name),
-                    'order': config.getint(name + '.order', 0),
-                    'label': config.get(name + '.label') or name.capitalize(),
-                    'value': config.get(name + '.value', '')
-                }
-                if field['type'] == 'select' or field['type'] == 'radio':
-                    field['options'] = config.getlist(name + '.options', sep='|')
-                    if '' in field['options']:
-                        field['optional'] = True
-                        field['options'].remove('')
-                elif field['type'] == 'text':
-                    field['format'] = config.get(name + '.format', 'plain')
-                elif field['type'] == 'textarea':
-                    field['format'] = config.get(name + '.format', 'plain')
-                    field['width'] = config.getint(name + '.cols')
-                    field['height'] = config.getint(name + '.rows')
-                fields.append(field)
-
-            fields.sort(lambda x, y: cmp(x['order'], y['order']))
-            
-            self.all_custom_fields[realm] = fields
-
-        self.env.log.debug("<<< custom_fields")
-            
-        return self.all_custom_fields[realm]
-
-    def _get_object_metadata(self, realm):
-        metadata = {}
-        
-        metadata['has_custom'] = self.SCHEMA[realm]['has_custom']
-        metadata['has_change'] = self.SCHEMA[realm]['has_change']
-        
-        return metadata
-
-        
-def _formatExceptionInfo(maxTBlevel=5):
-    cla, exc, trbk = sys.exc_info()
-    excName = cla.__name__
-    
-    try:
-        excArgs = exc.__dict__["args"]
-    except KeyError:
-        excArgs = "<no args>"
-    
-    excTb = traceback.format_tb(trbk, maxTBlevel)
-    return (excName, excArgs, excTb)

trunk/testmanager/sql.py

-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2010 Roberto Longobardi, Marco Cipriani
-#
-
-import re
-import sys
-import time
-import traceback
-
-from datetime import datetime
-from trac.core import *
-from trac.perm import IPermissionRequestor, PermissionError
-from trac.util.translation import _, N_, gettext
-from trac.web.api import IRequestHandler
-
-
-class SqlExecutor(Component):
-    """SQL Executor."""
-
-    implements(IPermissionRequestor, IRequestHandler)
-    
-    # IPermissionRequestor methods
-    def get_permission_actions(self):
-        return ['SQL_RUN']
-
-        
-    # IRequestHandler methods
-
-    def match_request(self, req):
-        return req.path_info.startswith('/sqlexec') and 'SQL_RUN' in req.perm
-
-    def process_request(self, req):
-        """Executes a generic SQL."""
-
-        req.perm.require('SQL_RUN')
-        
-        sql = req.args.get('sql')
-        print (sql)
-
-        try:
-            db = self.env.get_db_cnx()
-            cursor = db.cursor()
-            cursor.execute(sql)
-            
-            result = ''
-            for row in cursor:
-                for i in row:
-                    result += str(i) + ', '
-
-            db.commit()
-            
-            print(result)
-        except:
-            result = self._formatExceptionInfo()
-            db.rollback()
-            print("SqlExecutor - Exception: ")
-            print(result)
-            
-        
-        return 'result.html', {'result': result}, None
-
-        
-    def _formatExceptionInfo(maxTBlevel=5):
-        cla, exc, trbk = sys.exc_info()
-        excName = cla.__name__
-        
-        try:
-            excArgs = exc.__dict__["args"]
-        except KeyError:
-            excArgs = "<no args>"
-        
-        excTb = traceback.format_tb(trbk, maxTBlevel)
-        return (excName, excArgs, excTb)
- 

trunk/testmanager/stats.py

-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2010 Roberto Longobardi, Marco Cipriani
-#
-# The structure of this plugin is stolen from the Tracticketstats plugin, 
-# by Prentice Wongvibulisn
-
-import re
-
-from genshi.builder import tag
-
-from trac.core import *
-from trac.config import Option, IntOption
-from trac.web import IRequestHandler
-from trac.web.chrome import INavigationContributor, ITemplateProvider
-from trac.perm import IPermissionRequestor
-
-from datetime import date, datetime, time, timedelta
-from time import strptime
-from trac.util.datefmt import utc, to_timestamp
-
-from testmanager.api import TestManagerSystem
-from testmanager.util import *
-
-
-# ************************
-DEFAULT_DAYS_BACK = 30*6 
-DEFAULT_INTERVAL = 30
-# ************************
-
-class TestStatsPlugin(Component):
-    implements(INavigationContributor, IRequestHandler, ITemplateProvider, IPermissionRequestor)
-
-    yui_base_url = Option('teststats', 'yui_base_url',
-            default='http://yui.yahooapis.com/2.5.2',
-            doc='Location of YUI API')
-
-    default_days_back = IntOption('teststats', 'default_days_back',
-            default=DEFAULT_DAYS_BACK,
-            doc='Number of days to show by default')
-
-    default_interval = IntOption('teststats', 'default_interval',
-            default=DEFAULT_INTERVAL,
-            doc='Number of days between each data point'\
-                ' (resolution) by default')
-
-    # ==[ INavigationContributor methods ]==
-
-    def get_active_navigation_item(self, req):
-        return 'teststats'
-
-    def get_permission_actions(self):
-        return ['TEST_STATS_VIEW']
-
-    def get_navigation_items(self, req):
-        if req.perm.has_permission('TEST_STATS_VIEW'):
-            yield ('mainnav', 'teststats', 
-                tag.a('Test Stats', href=req.href.teststats()))
-
-    # ==[ Helper functions ]==
-    def _get_num_testcases(self, from_date, at_date, catpath, testplan, req):
-        '''
-        Returns an integer of the number of test cases 
-        counted between from_date and at_date.
-        '''
-
-        if catpath == None or catpath == '':
-            path_filter = "TC_%"
-        else:
-            path_filter = catpath + "_%" 
-
-        dates_condition = ''
-
-        if from_date:
-            dates_condition += " AND time > %s" % to_any_timestamp(from_date)
-
-        if at_date:
-            dates_condition += " AND time <= %s" % to_any_timestamp(at_date)
-
-        db = self.env.get_db_cnx()
-        cursor = db.cursor()
-        
-        # TODO: Fix this query because it also counts test catalogs
-        cursor.execute("SELECT COUNT(*) FROM wiki WHERE name LIKE '%s' AND version = 1 %s" % (path_filter, dates_condition))
-
-        row = cursor.fetchone()
-        
-        count = row[0]
-
-        return count
-
-
-    def _get_num_tcs_by_status(self, from_date, at_date, status, testplan, req):
-        '''
-        Returns an integer of the number of test cases that had the
-        specified status between from_date to at_date.
-        '''
-        
-        if testplan == None or testplan == '':
-            testplan_filter = ''
-        else:
-            testplan_filter = " AND planid = '%s'" % (testplan) 
-        
-        db = self.env.get_db_cnx()
-        cursor = db.cursor()
-
-        cursor.execute("SELECT COUNT(*) from testcasehistory WHERE status = '%s' AND time > %s AND time <= %s %s" % (status, to_timestamp(from_date), to_timestamp(at_date), testplan_filter))
-
-        row = cursor.fetchone()
-        
-        count = row[0]
-
-        return count
-
-
-    # ==[ IRequestHandle methods ]==
-
-    def match_request(self, req):
-        return re.match(r'/teststats(?:_trac)?(?:/.*)?$', req.path_info)
-
-    def process_request(self, req):
-        test_manager_system = TestManagerSystem(self.env)
-        #test_manager_system.report_testcase_status()
-        
-        req_content = req.args.get('content')
-        testplan = None
-        catpath = None
-        
-        if not None in [req.args.get('end_date'), req.args.get('start_date'), req.args.get('resolution')]:
-            # form submit
-            grab_at_date = req.args.get('end_date')
-            grab_from_date = req.args.get('start_date')
-            grab_resolution = req.args.get('resolution')
-            grab_testplan = req.args.get('testplan')
-            if grab_testplan and not grab_testplan == "__all":
-                testplan = grab_testplan.partition('|')[0]
-                catpath = grab_testplan.partition('|')[2]
-
-            # validate inputs
-            if None in [grab_at_date, grab_from_date]:
-                raise TracError('Please specify a valid range.')
-
-            if None in [grab_resolution]:
-                raise TracError('Please specify the graph interval.')
-            
-            if 0 in [len(grab_at_date), len(grab_from_date), len(grab_resolution)]:
-                raise TracError('Please ensure that all fields have been filled in.')
-
-            if not grab_resolution.isdigit():
-                raise TracError('The graph interval field must be an integer, days.')
-
-            # TODO: I'm letting the exception raised by 
-            #  strptime() appear as the Trac error.
-            #  Maybe a wrapper should be written.
-
-            at_date = datetime(*strptime(grab_at_date, "%m/%d/%Y")[0:6])
-            at_date = datetime.combine(at_date, time(11,59,59,0,utc)) # Add tzinfo
-
-            from_date = datetime(*strptime(grab_from_date, "%m/%d/%Y")[0:6])
-            from_date = datetime.combine(from_date, time(0,0,0,0,utc)) # Add tzinfo
-
-            graph_res = int(grab_resolution)
-
-        else:
-            # default data
-            todays_date = date.today()
-            at_date = datetime.combine(todays_date,time(11,59,59,0,utc))
-            from_date = at_date - timedelta(self.default_days_back)
-            graph_res = self.default_interval
-    
-            at_date_str = at_date.strftime("%m/%d/%Y")
-            from_date_str=  from_date.strftime("%m/%d/%Y")
-
-            # 2.5 only: at_date = datetime.strptime(at_date_str, "%m/%d/%Y")
-            at_date = datetime(*strptime(at_date_str, "%m/%d/%Y")[0:6])
-            at_date = datetime.combine(at_date, time(11,59,59,0,utc)) # Add tzinfo
-            
-            # 2.5 only: from_date = datetime.strptime(from_date_str, "%m/%d/%Y")
-            from_date = datetime(*strptime(from_date_str, "%m/%d/%Y")[0:6])
-            from_date = datetime.combine(from_date, time(0,0,0,0,utc)) # Add tzinfo
-            
-        count = []
-
-        # Calculate 0th point 
-        last_date = from_date - timedelta(graph_res)
-
-        # Calculate remaining points
-        for cur_date in daterange(from_date, at_date, graph_res):
-            num_new = self._get_num_testcases(last_date, cur_date, catpath, testplan, req)
-            num_successful = self._get_num_tcs_by_status(last_date, cur_date, 'SUCCESSFUL', testplan, req)
-            num_failed = self._get_num_tcs_by_status(last_date, cur_date, 'FAILED', testplan, req)
-            
-            num_all = self._get_num_testcases(None, cur_date, catpath, testplan, req)
-            num_all_successful = self._get_num_tcs_by_status(from_date, cur_date, 'SUCCESSFUL', testplan, req)
-            num_all_failed = self._get_num_tcs_by_status(from_date, cur_date, 'FAILED', testplan, req)
-
-            num_all_untested = num_all - num_all_successful - num_all_failed
-
-            datestr = cur_date.strftime("%m/%d/%Y") 
-            if graph_res != 1:
-                datestr = "%s thru %s" % (last_date.strftime("%m/%d/%Y"), datestr) 
-            count.append( {'from_date': last_date.strftime("%m/%d/%Y"),
-                         'to_date': cur_date.strftime("%m/%d/%Y"),
-                         'date'  : datestr,
-                         'new_tcs'    : num_new,
-                         'successful': num_successful,
-                         'failed': num_failed,
-                         'all_tcs'    : num_all,
-                         'all_successful': num_all_successful,
-                         'all_untested': num_all_untested,
-                         'all_failed': num_all_failed })
-            last_date = cur_date
-
-        # if chartdata is requested, raw text is returned rather than data object
-        # for templating
-        if (not req_content == None) and (req_content == "chartdata"):
-            jsdstr = '{"chartdata": [\n'
-            for x in count:
-                jsdstr += '{"date": "%s",' % x['date']
-                jsdstr += ' "new_tcs": %s,' % x['new_tcs']
-                jsdstr += ' "successful": %s,' % x['successful']
-                jsdstr += ' "failed": %s,' % x['failed']
-                jsdstr += ' "all_tcs": %s,' % x['all_tcs']
-                jsdstr += ' "all_successful": %s,' % x['all_successful']
-                jsdstr += ' "all_untested": %s,' % x['all_untested']
-                jsdstr += ' "all_failed": %s},\n' % x['all_failed']
-            jsdstr = jsdstr[:-2] +'\n]}'
-            req.send_header("Content-Length", len(jsdstr))
-            req.write(jsdstr)
-            return 
-        elif (not req_content == None) and (req_content == "downloadcsv"):
-            csvstr = "Date from;Date to;New Test Cases;Successful;Failed;Total Test Cases;Total Successful;Total Untested;Total Failed\r\n"
-            for x in count:
-                csvstr += '%s;' % x['from_date']
-                csvstr += '%s;' % x['to_date']
-                csvstr += '%s;' % x['new_tcs']
-                csvstr += '%s;' % x['successful']
-                csvstr += '%s;' % x['failed']
-                csvstr += '%s;' % x['all_tcs']
-                csvstr += '%s;' % x['all_successful']
-                csvstr += '%s;' % x['all_untested']
-                csvstr += '%s\r\n' % x['all_failed']
-            req.send_header("Content-Length", len(csvstr))
-            req.send_header("Content-Disposition", "attachment;filename=Test_stats.csv")
-            req.write(csvstr)
-            return 
-        else:
-            db = self.env.get_db_cnx()
-            showall = req.args.get('show') == 'all'
-
-            testplan_list = []
-            for planid, catid, catpath, name, author, ts_str in test_manager_system.list_all_testplans():
-                testplan_list.append({'planid': planid, 'catpath': catpath, 'name': name})
-
-            data = {}
-            data['testcase_data'] = count
-            data['start_date'] = from_date.strftime("%m/%d/%Y")
-            data['end_date'] = at_date.strftime("%m/%d/%Y")
-            data['resolution'] = str(graph_res)
-            data['baseurl'] = req.base_url
-            data['testplans'] = testplan_list
-            data['ctestplan'] = testplan
-            return 'testmanagerstats.html', data, None
- 
-    # ITemplateProvider methods
-    def get_templates_dirs(self):
-        """
-        Return the absolute path of the directory containing the provided
-        Genshi templates.
-        """
-        from pkg_resources import resource_filename
-        return [resource_filename(__name__, 'templates')]
-
-    def get_htdocs_dirs(self):
-        """Return the absolute path of a directory containing additional
-        static resources (such as images, style sheets, etc).
-        """
-        from pkg_resources import resource_filename
-        #return [('testmanager', resource_filename(__name__, 'htdocs'))]
-        return [('testmanager', resource_filename('testmanager', 'htdocs'))]