1. Olemis Lang
  2. testman4trac

Commits

seccanj  committed 519baa2

Divided the plugin into four different plugins. A generic persistent class support, a generic resource workflow engine, an SQL executor (for debugging) and the Test Manager, which leverages the first two.
The three plugins must be installed in this order:
* TracGenericClass
* TracGeneric Workflow
* TestManager

The SQLExecutor is just a hack allowing you to execute any SQL by writing it in the browser URL. It's intended for debugging purposes and should not be installed in a production environment.

  • Participants
  • Parent commits cfbb3e9
  • Branches testman4trac

Comments (0)

Files changed (60)

File build.sh

View file
+project_path=$1
+
+mkdir bin
+
+cd tracgenericclass/trunk
+python setup.py bdist_egg
+cp dist/*.egg ../../bin
+
+cd ../../tracgenericworkflow/trunk
+python setup.py bdist_egg
+cp dist/*.egg ../../bin
+
+cd ../../sqlexecutor/trunk
+python setup.py bdist_egg
+cp dist/*.egg ../../bin
+
+cd ../../testman4trac/trunk
+python setup.py bdist_egg
+cp dist/*.egg ../../bin
+cp testmanager/INSTALLATION.txt ../../bin
+
+cd ../..
+
+cp bin/*.egg $project_path/plugins
+

File sqlexecutor/trunk/setup.py

View file
+from setuptools import setup
+
+setup(
+    name='SQLExecutor',
+    version='1.0.0',
+    packages=['sqlexecutor'],
+    package_data={'sqlexecutor' : ['*.txt', 'templates/*.html', 'htdocs/*.*', 'htdocs/js/*.js', 'htdocs/css/*.css', 'htdocs/images/*.*']},
+    author = 'Roberto Longobardi',
+    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 - SQL Executor component',
+    long_description='A Trac plugin to create Test Cases, organize them in catalogs and track their execution status and outcome. This module provides a generic SQL executor to help debugging your application.',
+    keywords='trac plugin generic class framework persistence sql execution run test case management project quality assurance statistics stats charts charting graph',
+    entry_points = {'trac.plugins': ['sqlexecutor = sqlexecutor']}
+    )

File sqlexecutor/trunk/sqlexecutor/LICENSE.txt

View file
+Copyright (c) 2010, Roberto Longobardi
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+ - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

File sqlexecutor/trunk/sqlexecutor/README.txt

View file
+Trac Generic SQL Executor - Part of the Test Manager plugin for Trac
+
+  Copyright 2010 Roberto Longobardi
+
+  Project web page on TracHacks: http://trac-hacks.org/wiki/TestManagerForTracPlugin
+  
+  Project web page on SourceForge.net: http://sourceforge.net/projects/testman4trac/
+  
+  Project web page on Pypi: http://pypi.python.org/pypi/TestManager
+
+  
+A Trac plugin to create Test Cases, organize them in catalogs, generate test plans and track their execution status and outcome.
+
+This module provides a simple way of running any SQL in the context of the Trac environment. 
+This is an extremely helpful debug tool when dealing with database persistence.
+
+Caution: This plugin is not intended for a production environment.
+
+=================================================================================================  
+Change History:
+
+(Refer to the tickets on trac-hacks for complete descriptions.)
+
+Release 1.0.0 (2010-10-01):
+  o First release publicly available apart from the core Test Manager plugin
+  

File sqlexecutor/trunk/sqlexecutor/__init__.py

View file
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010 Roberto Longobardi
+#
+
+import sql

File sqlexecutor/trunk/sqlexecutor/htdocs/place.holder

Empty file added.

File sqlexecutor/trunk/sqlexecutor/sql.py

View file
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010 Roberto Longobardi
+#
+
+import re
+import sys
+import time
+import traceback
+
+from datetime import datetime
+from trac.core import *
+from trac.perm import IPermissionRequestor
+from trac.util.translation import _, N_, gettext
+from trac.web.api import IRequestHandler
+from trac.web.chrome import ITemplateProvider
+
+from tracgenericclass.util import *
+
+
+class SqlExecutor(Component):
+    """SQL Executor."""
+
+    implements(IPermissionRequestor, IRequestHandler, ITemplateProvider)
+    
+    # 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')
+        self.env.log.debug(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()
+            
+            self.env.log.debug(result)
+        except:
+            result = formatExceptionInfo()
+            db.rollback()
+            self.env.log.debug("SqlExecutor - Exception: ")
+            self.env.log.debug(result)
+        
+        return 'result.html', {'result': result}, 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 [('sqlexecutor', resource_filename(__name__, 'htdocs'))]
+
+       

File sqlexecutor/trunk/sqlexecutor/templates/empty.html

View file
+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://genshi.edgewall.org/"
+      xmlns:xi="http://www.w3.org/2001/XInclude">
+
+</html>

File sqlexecutor/trunk/sqlexecutor/templates/result.html

View file
+<html>
+  <body>
+    <p>Result:</p>
+    <br />
+    <div id="response">$result</div>
+  </body>
+</html>

File testman4trac/trunk/setup.py

View file
+from setuptools import setup
+
+setup(
+    name='TestManager',
+    version='1.3.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'],
+    )

File testman4trac/trunk/testmanager/INSTALLATION.txt

View file
+Installation:
+
+The functionalities are divided in three plugins, which must be installed in this order:
+
+    1) Trac Generic Class => TracGenericClass
+
+    2) Trac Generic Workflow => TracGenericWorkflow
+
+    3) Test Manager => TestManager
+
+
+An additional plugin is only useful for debugging and should not be installed in a production environment.
+
+    *  SQL Executor => SQLExecutor
+
+

File testman4trac/trunk/testmanager/LICENSE.txt

View file
+Copyright (c) 2010, Roberto Longobardi, Marco Cipriani
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+ - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

File testman4trac/trunk/testmanager/README.txt

View file
+Test Manager plugin for Trac
+
+  Copyright 2010 Roberto Longobardi
+
+  Project web page on TracHacks: http://trac-hacks.org/wiki/TestManagerForTracPlugin
+  
+  Project web page on SourceForge.net: http://sourceforge.net/projects/testman4trac/
+  
+  Project web page on Pypi: http://pypi.python.org/pypi/TestManager
+
+  
+A Trac plugin to create Test Cases, organize them in catalogs, generate test plans and track their execution status and outcome.
+
+Refer to INSTALL.txt for installation details.
+
+=================================================================================================  
+Change History:
+
+(Refer to the tickets on trac-hacks for complete descriptions.)
+
+Release 1.3.0 (2010-10-01):
+  o The base Test Manager plugin has been separated and other plugins have been created to embed the functionalities of generic class framework and the workflow engine.
+    Now we have four plugins:
+      * Trac Generic Class plugin, providing the persistent class framework
+      * Trac Generic Workflow plugin, providing the generic workflow engine applicable 
+        to any Trac Resource. This plugin requires the Trac Generic Class plugin.
+      * SQL Executor plugin, as a debugging tool when dealing with persistent data. Not to be enabled in a production environment.
+
+    Moreover, the generic class framework has been added a set of new functionalities. 
+    Refer to the README file in its package for more details.
+      
+Release 1.2.0 (2010-09-20):
+  o The data model has been completely rewritten, now using python classes for all the test objects.
+    A generic object supporting programmatic definition of its standard fields, declarative 
+    definition of custom fields (in trac.ini) and keeping track of change history has been created, 
+    by generalizing the base Ticket code.
+    
+    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.
+    
+  o Enhancement #7704 Add workflow capabilities, with custom states, transitions and operations, and state transition listeners support
+      A generic Trac Resource workflow system has been implemented, allowing to add workflow capabilities 
+      to any Trac resource.
+      Test objects have been implemented as Trac resources as well, so they benefit of workflow capabilities.
+
+      Available objects 'realms' to associate workflows to are: testcatalog, testcase, testcaseinplan, testplan.
+      
+      Note that the object with realm 'resourceworkflowstate', which manages the state of any resource in a
+      workflow, also supports custom properties (see below), so plugins can augment a resource workflow state
+      with additional context information and use it inside listener callbacks, for example.
+
+      For example, add the following to your trac.ini file to associate a workflow with all Test Case objects.
+      The sample_operation is currently provided by the Test Manager system itself, as an example.
+      It just logs a debug message with the text input by the User in a text field.
+      
+        [testcase-resource_workflow]
+        leave = * -> *
+        leave.operations = sample_operation
+        leave.default = 1
+
+        accept = new -> accepted
+        accept.permissions = TEST_MODIFY
+        accept.operations = sample_operation
+
+        resolve = accepted -> closed
+        resolve.permissions = TEST_MODIFY
+        resolve.operations = sample_operation
+
+  o Enhancement #7705 Add support for custom properties and change history to all of the test management objects
+      A generic object supporting programmatic definition of its standard fields, declarative definition 
+      of custom fields (in trac.ini) and keeping track of change history has been created, by generalizing 
+      the base Ticket code.
+      
+      Only text type of properties are currently supported.
+
+      For example, add the following to your trac.ini file to add custom properties to all of the four
+      test objects.
+      Note that the available realms to augment are, as above, testcatalog, testcase, testcaseinplan and testplan, 
+      with the addition of resourceworkflowstate.
+
+        [testcatalog-tm_custom]
+        prop1 = text
+        prop1.value = Default value
+
+        [testcaseinplan-tm_custom]
+        prop_strange = text
+        prop_strange.value = windows
+
+        [testcase-tm_custom]
+        nice_prop = text
+        nice_prop.value = My friend
+
+        [testplan-tm_custom]
+        good_prop = text
+        good_prop.value = linux
+
+  o Enhancement #7569 Add listener interface to let other components react to test case status change
+      Added listener interface for all of the test objects lifecycle:
+       * Object created
+       * Object modified (including custom properties)
+       * Object deleted
+      This applies to test catalogs, test cases, test plans and test cases in a plan (i.e. with a status).
+  
+Release 1.1.2 (2010-08-25):
+  o Enhancement #7552 Export test statistics in CSV and bookmark this chart features in the test stats chart
+  o Fixed Ticket #7551 Test statistics don't work on Trac 0.11
+
+Release 1.1.1 (2010-08-20):
+  o Enhancement #7526 Add ability to duplicate a test case
+  o Enhancement #7536 Add test management statistics
+  o Added "autosave=true" parameter to the RESTful API to create test catalogs 
+    and test cases programmatically without need to later submit the wiki editing form.
+
+Release 1.1.0 (2010-08-18):
+  o Enhancement #7487 Add multiple test plans capability
+  o Enhancement #7507 Implement security permissions
+  o Enhancement #7484 Reverse the order of changes in the test case status change history
+
+Release 1.0.2 (2010-08-17):
+  o Fixed Ticket #7485 "Open ticket on this test case" should work without a patched TracTicketTemplatePlugin
+
+Release 1.0.1 (2010-08-12):
+  o First attempt at externalizing strings
+
+Release 1.0 (2010-08-10):
+  o First release publicly available
+  

File testman4trac/trunk/testmanager/__init__.py

View file
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010 Roberto Longobardi, Marco Cipriani
+#
+
+"""
+See testmanager.api for detailed information.
+"""
+
+import api
+import macros
+import web_ui
+import wiki
+import model
+import util
+import stats
+import workflow

File testman4trac/trunk/testmanager/api.py

View file
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010 Roberto Longobardi
+#
+
+import re
+import sys
+import time
+import traceback
+
+from datetime import datetime
+from trac.core import *
+from trac.perm import IPermissionRequestor, PermissionError
+from trac.resource import Resource, IResourceManager, render_resource_link, get_resource_url
+from trac.util import get_reporter_id
+from trac.util.datefmt import utc
+from trac.util.translation import _, N_, gettext
+from trac.web.api import IRequestHandler
+
+from tracgenericclass.model import GenericClassModelProvider
+from tracgenericclass.util import *
+
+from testmanager.labels import *
+from testmanager.model import TestCatalog, TestCase, TestCaseInPlan, TestPlan
+
+
+class TestManagerSystem(Component):
+    """Test Manager system for Trac."""
+
+    implements(IPermissionRequestor, IRequestHandler, IResourceManager)
+
+    def get_next_id(self, type):
+        propname = _get_next_prop_name(type)
+    
+        try:
+            # Get next ID
+            db = self.env.get_db_cnx()
+            cursor = db.cursor()
+            sql = "SELECT value FROM testconfig WHERE propname='"+propname+"'"
+            
+            cursor.execute(sql)
+            row = cursor.fetchone()
+            
+            id = int(row[0])
+
+            # Increment next ID
+            cursor = db.cursor()
+            cursor.execute("UPDATE testconfig SET value='" + str(id+1) + "' WHERE propname='"+propname+"'")
+            
+            db.commit()
+        except:
+            self.env.log.debug(formatExceptionInfo())
+            db.rollback()
+            raise
+
+        return str(id)
+    
+    def set_next_id(self, type, value):
+        propname = _get_next_prop_name(type)
+        
+        try:
+            # Set next ID to the input value
+            db = self.env.get_db_cnx()
+            cursor = db.cursor()
+            cursor.execute("UPDATE testconfig SET value='" + str(value) + "' WHERE propname='"+propname+"'")
+           
+            db.commit()
+        except:
+            self.env.log.debug(formatExceptionInfo())
+            db.rollback()
+            raise
+    
+    def get_testcase_status_history_markup(self, id, planid):
+        """Returns a test case status in a plan audit trail."""
+
+        result = '<table class="listing"><thead>'
+        result += '<tr><th>'+LABELS['timestamp']+'</th><th>'+LABELS['author']+'</th><th>'+LABELS['status']+'</th></tr>'
+        result += '</thead><tbody>'
+        
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+
+        sql = "SELECT time, author, status FROM testcasehistory WHERE id='"+str(id)+"' AND planid='"+str(planid)+"' ORDER BY time DESC"
+        
+        cursor.execute(sql)
+        for ts, author, status in cursor:
+            result += '<tr>'
+            result += '<td>'+str(from_any_timestamp(ts))+'</td>'
+            result += '<td>'+author+'</td>'
+            result += '<td>'+LABELS[status]+'</td>'
+            result += '</tr>'
+
+        result += '</tbody></table>'
+         
+        return result
+        
+        
+    # @deprecated
+    def list_all_testplans(self):
+        """Returns a list of all test plans."""
+
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+
+        sql = "SELECT id, catid, page_name, name, author, time FROM testplan ORDER BY catid, id"
+        
+        cursor.execute(sql)
+        for id, catid, page_name, name, author, ts  in cursor:
+            yield id, catid, page_name, name, author, str(from_any_timestamp(ts))
+
+
+    # IPermissionRequestor methods
+    def get_permission_actions(self):
+        return ['TEST_VIEW', 'TEST_MODIFY', 'TEST_EXECUTE', 'TEST_DELETE', 'TEST_PLAN_ADMIN']
+
+        
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        type = req.args.get('type', '')
+        
+        match = False
+        
+        if req.path_info.startswith('/testcreate') and (((type == 'catalog' or type == 'testcase') and ('TEST_MODIFY' in req.perm)) or 
+             (type == 'testplan' and ('TEST_PLAN_ADMIN' in req.perm))):
+            match = True
+        elif (req.path_info.startswith('/teststatusupdate') and 'TEST_EXECUTE' in req.perm):
+            match = True
+        
+        return match
+
+    def process_request(self, req):
+        """
+        Handles Ajax requests to set the test case status and 
+        to create test objects.
+        """
+        author = get_reporter_id(req, 'author')
+
+        if req.path_info.startswith('/teststatusupdate'):
+            req.perm.require('TEST_EXECUTE')
+        
+            id = req.args.get('id')
+            planid = req.args.get('planid')
+            path = req.args.get('path')
+            status = req.args.get('status')
+
+            try:
+                self.env.log.debug("Setting status %s to test case %s in plan %s" % (status, id, planid))
+                tcip = TestCaseInPlan(self.env, id, planid)
+                if tcip.exists:
+                    tcip.set_status(status, author)
+                    tcip.save_changes(author, "Status changed")
+                else:
+                    tcip['page_name'] = path
+                    tcip['status'] = status
+                    tcip.insert()
+                
+            except:
+                self.env.log.debug(formatExceptionInfo())
+        
+        elif req.path_info.startswith('/testcreate'):
+            type = req.args.get('type')
+            path = req.args.get('path')
+            title = req.args.get('title')
+            author = get_reporter_id(req, 'author')
+
+            autosave = req.args.get('autosave', 'false')
+            duplicate = req.args.get('duplicate')
+            paste = req.args.get('paste')
+            tcId = req.args.get('tcId')
+
+            id = self.get_next_id(type)
+
+            pagename = path
+            
+            if type == 'catalog':
+                req.perm.require('TEST_MODIFY')
+                pagename += '_TT'+str(id)
+
+                try:
+                    new_tc = TestCatalog(self.env, id, pagename, title, '')
+                    new_tc.author = author
+                    new_tc.remote_addr = req.remote_addr
+                    # This also creates the Wiki page
+                    new_tc.insert()
+                    
+                except:
+                    print "Error adding test catalog!"
+                    print formatExceptionInfo()
+                    req.redirect(req.path_info)
+
+                # Redirect to see the new wiki page.
+                req.redirect(req.href.wiki(pagename))
+                
+            elif type == 'testplan':
+                req.perm.require('TEST_PLAN_ADMIN')
+                
+                catid = path.rpartition('_TT')[2]
+
+                try:
+                    # Add the new test plan in the database
+                    new_tc = TestPlan(self.env, id, catid, pagename, title, author)
+                    new_tc.insert()
+
+                except:
+                    print "Error adding test plan!"
+                    print formatExceptionInfo()
+                    # Back to the catalog
+                    req.redirect(req.href.wiki(path))
+
+                # Display the new test plan
+                req.redirect(req.href.wiki(path, planid=str(id)))
+                    
+            elif type == 'testcase':
+                req.perm.require('TEST_MODIFY')
+                
+                pagename += '_TC'+str(id)
+                
+                if paste and paste != '':
+                    # Handle move/paste of the test case into another catalog
+
+                    req.perm.require('TEST_PLAN_ADMIN')
+
+                    try:
+                        catid = path.rpartition('_TT')[2]
+                        tcat = TestCatalog(self.env, catid)
+                        
+                        old_pagename = tcId
+                        tc_id = tcId.rpartition('_TC')[2]
+                        tc = TestCase(self.env, tc_id, tcId)
+                        if tc.exists:
+                            tc.move_to(tcat)                            
+                        else:
+                            self.env.log.debug("Test case not found")
+                    except:
+                        self.env.log.debug("Error pasting test case!")
+                        self.env.log.debug(formatExceptionInfo())
+                        req.redirect(req.path_info)
+                
+                    # Redirect to test catalog, forcing a page refresh by means of a random request parameter
+                    req.redirect(req.href.wiki(pagename.rpartition('_TC')[0], random=str(datetime.now(utc).microsecond)))
+                    
+                elif duplicate and duplicate != '':
+                    # Duplicate test case
+                    old_id = tcId.rpartition('_TC')[2]
+                    old_pagename = tcId
+                    try:
+                        old_tc = TestCase(self.env, old_id, old_pagename)
+                        
+                        # New test case name will be the old catalog name + the newly generated test case ID
+                        author = get_reporter_id(req, 'author')
+                        
+                        # Create new test case wiki page as a copy of the old one, but change its page path
+                        new_tc = old_tc
+                        new_tc['page_name'] = pagename
+                        new_tc.remote_addr = req.remote_addr
+                        # And save it under the new id
+                        new_tc.save_as({'id': id})
+                        
+                    except:
+                        self.env.log.debug("Error duplicating test case!")
+                        self.env.log.debug(formatExceptionInfo())
+                        req.redirect(req.path_info)
+
+                    # Redirect tp allow for editing the copy test case
+                    req.redirect(req.href.wiki(pagename, action='edit'))
+                    
+                else:
+                    # Normal creation of a new test case
+                    try:
+                        new_tc = TestCase(self.env, id, pagename, title, '')
+                        new_tc.author = author
+                        new_tc.remote_addr = req.remote_addr
+                        # This also creates the Wiki page
+                        new_tc.insert()
+                        
+                    except:
+                        self.env.log.debug("Error adding test case!")
+                        self.env.log.debug(formatExceptionInfo())
+                        req.redirect(req.path_info)
+
+                    # Redirect to edit the test case description
+                    req.redirect(req.href.wiki(pagename, action='edit'))
+        
+        return 'empty.html', {}, None
+
+
+    # IResourceManager methods
+    
+    def get_resource_realms(self):
+        yield 'testcatalog'
+        yield 'testcase'
+        yield 'testcaseinplan'
+        yield 'testplan'
+
+    def get_resource_url(self, resource, href, **kwargs):
+        self.env.log.debug(">>> get_resource_url - %s" % resource)
+        
+        tmmodelprovider = GenericClassModelProvider(self.env)
+        obj = tmmodelprovider.get_object(resource.realm, get_dictionary_from_string(resource.id))
+        
+        if obj and obj.exists:
+            args = {}
+            
+            if resource.realm == 'testcaseinplan':
+                args = {'planid': obj['planid']}
+            elif resource.realm == 'testplan':
+                args = {'planid': obj['id']}
+
+            args.update(kwargs)
+                 
+            self.env.log.debug("<<< get_resource_url - exists")
+
+            return href('wiki', obj['page_name'], **args)
+        else:
+            self.env.log.debug("<<< get_resource_url - does NOT exist")
+            return href('wiki', 'TC', **kwargs)
+
+    def get_resource_description(self, resource, format='default', context=None,
+                                 **kwargs):
+        return resource.id
+
+    def resource_exists(self, resource):
+        tmmodelprovider = GenericClassModelProvider(self.env)
+        obj = tmmodelprovider.get_object(resource.realm, get_dictionary_from_string(resource.id))
+        
+        return obj.exists
+
+        
+def _get_next_prop_name(type):
+    propname = ''
+
+    if type == 'catalog':
+        propname = 'NEXT_CATALOG_ID'
+    elif type == 'testcase':
+        propname = 'NEXT_TESTCASE_ID'
+    elif type == 'testplan':
+        propname = 'NEXT_PLAN_ID'
+
+    return propname
+        

File testman4trac/trunk/testmanager/htdocs/css/testmanager.css

View file
+ul {
+    list-style-type: none;
+}
+
+li {
+    margin-left: -10px;
+    padding: 1px;
+} 
+
+.iconElement {
+    position: relative; 
+    top: 4px;
+    margin-right: 3px;
+}
+
+.rightIcon {
+    background: transparent url(../images/pencil.png) no-repeat scroll 0 0;
+    padding-right:25px;
+}
+
+.messageBox {
+    background-color: #FFFF99; 
+    border: 1px solid #FFCC35; 
+    font-weight: bold;
+}
+
+.ninja {
+    color: black;
+    visibility: hidden;
+}

File testman4trac/trunk/testmanager/htdocs/images/empty.png

Added
New image

File testman4trac/trunk/testmanager/htdocs/images/gray.png

Added
New image

File testman4trac/trunk/testmanager/htdocs/images/green.png

Added
New image

File testman4trac/trunk/testmanager/htdocs/images/minus.png

Added
New image

File testman4trac/trunk/testmanager/htdocs/images/pencil.png

Added
New image

File testman4trac/trunk/testmanager/htdocs/images/plus.png

Added
New image

File testman4trac/trunk/testmanager/htdocs/images/red.png

Added
New image

File testman4trac/trunk/testmanager/htdocs/images/yellow.png

Added
New image

File testman4trac/trunk/testmanager/htdocs/js/cookies.js

View file
+/**
+ * Returns the value of the specified cookie, or null if the cookie is not found or has no value.
+ */
+function getCookie( name ) {
+	var allCookies = document.cookie.split( ';' );
+	var tempCookie = '';
+	var cookieName = '';
+	var cookieValue = '';
+	var found = false;
+	var i = '';
+	
+	for ( i = 0; i < allCookies.length; i++ ) {
+		/* split name=value pairs */
+		tempCookie = allCookies[i].split( '=' );
+		
+		
+		/* trim whitespace */
+		cookieName = tempCookie[0].replace(/^\s+|\s+$/g, '');
+	
+		if ( cookieName == name ) {
+			found = true;
+			/* handle case where cookie has no value (i.e. no equal sign)  */
+			if ( tempCookie.length > 1 ) {
+				cookieValue = unescape( tempCookie[1].replace(/^\s+|\s+$/g, '') );
+			}
+            
+			return cookieValue;
+			break;
+		}
+		tempCookie = null;
+		cookieName = '';
+	}
+    
+	if ( !found ) {
+		return null;
+	}
+}
+
+/**
+ * Sets a cookie with the input name and value.
+ * These are the only required parameters.
+ * The expires parameter must be expressed in hours.
+ * Generally you don't need to worry about domain, path or secure for most applications.
+ * In these cases, do not pass null values, but empty strings.
+ */
+function setCookie( name, value, expires, path, domain, secure ) {
+	var today = new Date();
+	today.setTime( today.getTime() );
+
+	if ( expires ) {
+		expires = expires * 1000 * 60 * 60;
+	}
+
+	var expires_date = new Date( today.getTime() + (expires) );
+
+	document.cookie = name + "=" +escape( value ) +
+		( ( expires ) ? ";expires=" + expires_date.toGMTString() : "" ) +
+		( ( path ) ? ";path=" + path : "" ) + 
+		( ( domain ) ? ";domain=" + domain : "" ) +
+		( ( secure ) ? ";secure" : "" );
+}
+
+/**
+ * Deletes the specified cookie
+ */
+function deleteCookie( name, path, domain ) {
+	if ( getCookie( name ) ) {
+        document.cookie = name + "=" +
+			( ( path ) ? ";path=" + path : "") +
+			( ( domain ) ? ";domain=" + domain : "" ) +
+			";expires=Thu, 01-Jan-1970 00:00:01 GMT";
+    }
+}

File testman4trac/trunk/testmanager/htdocs/js/labels.js

View file
+/*- coding: utf-8
+ *
+ * Copyright (C) 2010 Roberto Longopbardi - seccanj@gmail.com, Marco Cipriani
+ */
+
+var messages = {
+    'name_help': "You must specify a name. Length between 4 and 90 characters.",
+    'length_error': "Length between 4 and 90 characters."
+};
+
+var labels = {
+    'results': "Results: "
+};
+

File testman4trac/trunk/testmanager/htdocs/js/labels_en.js

View file
+/*- coding: utf-8
+ *
+ * Copyright (C) 2010 Roberto Longopbardi - seccanj@gmail.com, Marco Cipriani
+ */
+
+var messages = {
+    'name_help': "You must specify a name. Length between 4 and 90 characters.",
+    'length_error': "Length between 4 and 90 characters."
+};
+
+var labels = {
+    'results': "Results: "
+};
+

File testman4trac/trunk/testmanager/htdocs/js/labels_it.js

View file
+/*- coding: utf-8
+ *
+ * Copyright (C) 2010 Roberto Longopbardi - seccanj@gmail.com, Marco Cipriani
+ */
+
+var messages = {
+    'name_help': "Devi indicare un nome. Lunghezza da 4 a 90 caratteri.",
+    'length_error': "Lunghezza da 4 a 90 caratteri."
+};
+
+var labels = {
+    'results': "Risultati: "
+};
+

File testman4trac/trunk/testmanager/htdocs/js/testmanager.js

View file
+/*- coding: utf-8
+ *
+ * Copyright (C) 2010 Roberto Longopbardi - seccanj@gmail.com, Marco Cipriani
+ */
+
+/******************************************************/
+/**         Test case, catalog, plan creation         */
+/******************************************************/
+
+function creaTestCatalog(path) {
+	var tcInput = document.getElementById("catName");
+	var catalogName = tcInput.value;
+    
+    if (catalogName == null || catalogName.length == 0) {
+		document.getElementById('catErrorMsgSpan').innerHTML = messages['name_help'];
+    } else {
+    	var catName = stripLessSpecialChars(catalogName);
+    	
+    	if (catName.length > 90 || catName.length < 4) {
+    		document.getElementById('catErrorMsgSpan').innerHTML = messages['length_error'];
+    	} else { 
+    		document.getElementById('catErrorMsgSpan').innerHTML = ''; 
+    		var url = baseLocation+"/testcreate?type=catalog&path="+path+"&title="+catName;
+    		window.location = url;
+    	}
+    }
+}
+
+function creaTestCase(catName){ 
+	var tcInput = document.getElementById('tcName');
+	var testCaseName = tcInput.value; 
+
+    if (testCaseName == null || testCaseName.length == 0) {
+		document.getElementById('errorMsgSpan').innerHTML = messages['name_help'];
+    } else {
+    	var tcName = stripLessSpecialChars(testCaseName); 
+    	
+    	if (tcName.length > 90 || tcName.length < 4) {
+    		document.getElementById('errorMsgSpan').innerHTML = messages['length_error'];
+    	} else { 
+    		document.getElementById('errorMsgSpan').innerHTML = ''; 
+    		var url = baseLocation+"/testcreate?type=testcase&path="+catName+"&title="+tcName;
+    		window.location = url;
+    	}
+    }
+}
+
+function creaTestPlan(catName){ 
+	var planInput = document.getElementById('planName');
+	var testPlanName = planInput.value; 
+
+    if (testPlanName == null || testPlanName.length == 0) {
+		document.getElementById('errorMsgSpan2').innerHTML = messages['name_help'];
+    } else {
+    	var tplanName = stripLessSpecialChars(testPlanName); 
+    	
+    	if (tplanName.length > 90 || tplanName.length < 4) {
+    		document.getElementById('errorMsgSpan2').innerHTML = messages['length_error'];
+    	} else { 
+    		document.getElementById('errorMsgSpan2').innerHTML = ''; 
+    		var url = baseLocation+"/testcreate?type=testplan&path="+catName+"&title="+tplanName;
+    		window.location = url;
+    	}
+    }
+}
+
+function duplicateTestCase(tcName, catName){ 
+	var url = baseLocation+'/testcreate?type=testcase&duplicate=true&tcId='+tcName+'&path='+catName; 
+	window.location = url;
+}
+
+function regenerateTestPlan(planId, path) {
+    var url = baseLocation+"/testcreate?type=testplan&update=true&planid="+planId+"&path="+path;
+    window.location = url;
+}
+
+function creaTicket(tcName, planId, planName){ 
+	var url = baseLocation+'/newticket?testCaseNumber='+tcName+'&planId='+planId+'&planName='+planName+'&description=Test%20Case:%20[wiki:'+tcName+'?planid='+planId+'],%20Test%20Plan:%20'+planName+'%20('+planId+')'; 
+	window.location = url;
+}
+
+/******************************************************/
+/**         Move test case into another catalog       */
+/******************************************************/
+
+function checkMoveTCDisplays() {
+    displayNode('copiedTCMessage', isPasteEnabled());
+    displayNode('pasteTCHereMessage', isPasteEnabled());
+    displayNode('pasteTCHereDiv', isPasteEnabled());
+}
+
+function isPasteEnabled() {
+    if (getCookie('TestManager_TestCase')) {
+        return true;
+    }
+    
+    return false;
+}
+
+function copyTestCaseToClipboard(tcId) {
+    setCookie('TestManager_TestCase', tcId, 1, '/', '', '');
+    setTimeout('window.location="'+window.location+'"', 100);
+}
+
+function pasteTestCaseIntoCatalog(catName) {
+    var tcId = getCookie('TestManager_TestCase');
+    
+    if (tcId != null) {
+        deleteCookie('TestManager_TestCase', '/', '');
+        var url = baseLocation+"/testcreate?type=testcase&paste=true&path="+catName+"&tcId="+tcId;
+        window.location = url;
+    }
+}
+
+function cancelTCMove() {
+    deleteCookie('TestManager_TestCase', '/', '');
+    setTimeout('window.location="'+window.location+'"', 100);
+}
+
+/******************************************************/
+/**                 Tree view widget                  */
+/******************************************************/
+
+/** Configuration property to specify whether non-matching search results should be hidden. */ 
+var selectHide = true;
+/** Configuration property to specify whether matching search results should be displayed in bold font. */
+var selectBold = true;
+
+var selectData = [];
+var deselectData = [];
+var htimer = null;
+var searchResults = 0;
+
+function toggleAll(isexpand) {
+    var nodes=document.getElementById("ticketContainer").getElementsByTagName("span");
+    for(var i=0;i<nodes.length;i++) {
+        if(nodes.item(i).getAttribute("name") === "toggable") {
+            if (isexpand) {
+                expand(nodes.item(i).id);
+            } else {
+                collapse(nodes.item(i).id);
+            }
+        }
+    }
+}
+
+function collapse(id) {
+    el = document.getElementById(id);
+    if (el.getAttribute("name") === "toggable") {
+        el.firstChild['expanded'] = false;
+        el.firstChild.innerHTML = '<img class="iconElement" src="'+baseLocation+'/chrome/testmanager/images/plus.png" />';
+        document.getElementById(el.id+"_list").style.display = "none";
+    }
+}
+
+function expand(id) {
+    el = document.getElementById(id);
+    if (el.getAttribute("name") === "toggable") {
+        el.firstChild['expanded'] = true;
+        el.firstChild.innerHTML = '<img class="iconElement" src="'+baseLocation+'/chrome/testmanager/images/minus.png" />';
+        document.getElementById(el.id+"_list").style.display = "";
+    }
+}
+
+function toggle(id) {
+    var el=document.getElementById(id);
+    if (el.firstChild['expanded']) {
+        collapse(id)
+    } else {
+        expand(id)
+    }
+}
+
+function highlight(string) {
+    clearSelection();
+    if (string && string !== "") {
+        var res=[];
+        var tks=string.split(" ");
+        for (var i=0;i<tks.length;i++) {
+            res[res.length]=new RegExp(regexpescape(tks[i].toLowerCase()), "g");
+        }
+        var nodes=document.getElementById("ticketContainer").getElementsByTagName("a");
+        for(var i=0;i<nodes.length;i++) {
+            var n=nodes.item(i);
+            if (n.nextSibling) {
+                if (filterMatch(n, n.nextSibling, res)) {
+                    select(n);
+                } else {
+                    deselect(n);
+                }
+            }
+        }
+
+        document.getElementById('searchResultsNumberId').innerHTML = labels['results']+searchResults;
+    }
+}
+
+function regexpescape(text) {
+    return text.replace(/[-[\]{}()+?.,\\\\^$|#\s]/g, "\\\\$&").replace(/\*/g,".*");
+}
+
+function filterMatch(node1,node2,res) {
+    var name=(node1.innerHTML + node2.innerHTML).toLowerCase();
+    var match=true;
+    for (var i=0;i<res.length;i++) {
+        match=match && name.match(res[i]);
+    } 
+    return match;
+}
+
+function clearSelection() {
+    toggleAll(false);
+    for (var i=0;i<selectData.length;i++) {
+        selectData[i].style.fontWeight="normal";
+        selectData[i].style.display=""
+    };
+    
+    selectData=[];
+    
+    for (var i=0;i<deselectData.length;i++) {
+        if (selectHide) {
+            deselectData[i].style.display=""
+        }
+    };
+    
+    deselectData=[];
+    searchResults = 0;
+    
+    document.getElementById("searchResultsNumberId").innerHTML = '';
+}
+
+function select(node) {
+    searchResults++;
+
+    do {
+        if(node.tagName ==="UL" && node.id.indexOf("b_") === 0) {
+            expand(node.id.substr(0,node.id.indexOf("_list")));
+        };
+
+        if(node.tagName === "LI") {
+            if (selectBold) {
+                node.style.fontWeight = "bold";
+            };
+            
+            if (selectHide) {
+                node.style.display = "block";
+            };
+            
+            selectData[selectData.length]=node;
+        };
+        node=node.parentNode;
+    } while (node.id!=="ticketContainer");
+}
+
+function deselect(node) {
+    do {
+        if (node.tagName === "LI") {
+            if (selectHide && node.style.display==="") {
+                node.style.display = "none";
+                deselectData[deselectData.length]=node;
+            }
+        };
+        
+        node=node.parentNode;
+    } while (node.id!=="ticketContainer");
+}
+
+function starthighlight(string,now) {
+    if (htimer) {
+        clearTimeout(htimer);
+    } 
+    if (now) {
+        highlight(string);
+    } else {
+        htimer = setTimeout(function() {
+                                highlight(string);
+                            },500);
+    }
+}
+
+function checkFilter(now) {
+    var f=document.getElementById("tcFilter");
+    if (f) {
+        starthighlight(f.value,now);
+    }
+}
+
+function underlineLink(id) {
+    el = document.getElementById(id);
+    el.style.backgroundColor = '#EEEEEE';
+    el.style.color = '#BB0000';
+    el.style.textDecoration = 'underline';
+}
+
+function removeUnderlineLink(id) {
+    el = document.getElementById(id);
+    el.style.backgroundColor = 'white';
+    el.style.color = 'black';
+    el.style.textDecoration = 'none';
+}
+
+function showPencil(id) {
+    el = document.getElementById(id);
+    el.style.display = '';
+}
+
+function hidePencil(id) {
+    el = document.getElementById(id);
+    el.style.display = 'none';
+}
+
+/******************************************************/
+/**        Test case in plan status management        */
+/******************************************************/
+
+function changestate(tc, planid, path, newStatus) {
+
+    var url = baseLocation+"/teststatusupdate?id="+tc+"&planid="+planid+"&status="+newStatus+"&path="+path;
+    
+    result = doAjaxCall(url); 
+    
+    oldIconSpan = document.getElementById("tcStatus"+currStatus);
+    oldIconSpan.style.border="";
+    
+    newIconSpan = document.getElementById("tcStatus"+newStatus);
+    newIconSpan.style.border="2px solid black";
+    
+    displayNode("tcTitleStatusIcon"+currStatus, false);
+    displayNode("tcTitleStatusIcon"+newStatus, true);
+    
+    currStatus = newStatus; 
+}
+
+/******************************************************/
+/**                  Utility functions                */
+/******************************************************/
+
+function expandCollapseSection(nodeId) {
+    toggleClass(nodeId, "collapsed");
+}
+
+function stripSpecialChars(str) {
+    result = str.replace(/[ ',;:àèéìòù£§<>!"%&=@#��\[\]\-\\\\^\$\.\|\?\*\+\(\)\{\}]/g, '');
+    return result;
+}
+
+function stripLessSpecialChars(str) {
+    result = str.replace(/[;#&\?]/g, '');
+    return result;
+}
+
+function displayNode(id, show) {
+    var msgNode = document.getElementById(id);
+    if (msgNode) {
+        msgNode.style.display = show ? "block" : "none";
+    }
+}
+
+function toggleClass(nodeId, className) {
+    var node = document.getElementById(nodeId);
+    if (node.className === "") {
+        node.className = className;
+    } else {
+        node.className = "";
+    }
+}
+
+function doAjaxCall(url) {
+    if (window.XMLHttpRequest) {
+        /* code for IE7+, Firefox, Chrome, Opera, Safari */
+         xmlhttp = new XMLHttpRequest();
+    } else {
+        /* code for IE6, IE5 */
+        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
+    }
+    
+    xmlhttp.open("GET", url, false);
+    xmlhttp.send("");
+    responseText = xmlhttp.responseText;
+    
+    return responseText;
+}
+
+function editField(name) {
+    displayNode('custom_field_value_'+name, false);
+    displayNode('custom_field_'+name, true);
+    displayNode('update_button_'+name, true);
+}
+
+function sendUpdate(realm, name) {
+   	var objKeyField = document.getElementById("obj_key_field");
+    var objKey = objKeyField.value;
+
+   	var objPropsField = document.getElementById("obj_props_field");
+    var objProps = objPropsField.value;
+
+   	var inputField = document.getElementById("custom_field_"+name);
+	var value = inputField.value;
+    
+    var url = baseLocation+"/propertyupdate?realm="+realm+"&key="+objKey+"&props="+objProps+"&name="+name+"&value="+value;
+    
+    result = doAjaxCall(url); 
+
+   	var readonlyField = document.getElementById("custom_field_value_"+name);
+    readonlyField.innerHTML = value;
+
+    displayNode('custom_field_value_'+name, true);
+    displayNode('custom_field_'+name, false);
+    displayNode('update_button_'+name, false);
+}
+
+/**
+ * Adds the specified function, by name or by pointer, to the window.onload() queue.
+ * 
+ * Usage:
+ *
+ * addLoadHandler(nameOfSomeFunctionToRunOnPageLoad); 
+ *
+ * addLoadHandler(function() { 
+ *   <more code to run on page load>
+ * }); 
+ */
+function addLoadHandler(func) { 
+    var oldonload = window.onload; 
+    if (typeof window.onload != 'function') { 
+        window.onload = func; 
+    } else { 
+        window.onload = function() { 
+            if (oldonload) { 
+                oldonload(); 
+            } 
+            func(); 
+        } 
+    } 
+} 
+
+/**
+ * Do some checks as soon as the page is loaded.
+ */
+addLoadHandler(function() {
+        checkFilter(true);
+        checkMoveTCDisplays();
+    });

File testman4trac/trunk/testmanager/labels.py

View file
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010 Roberto Longobardi - seccanj@gmail.com
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution.
+#
+
+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"
+}

File testman4trac/trunk/testmanager/labels_en.py

View file
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010 Roberto Longobardi - seccanj@gmail.com
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution.
+#
+
+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"
+}

File testman4trac/trunk/testmanager/labels_it.py

View file
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010 Roberto Longobardi - seccanj@gmail.com
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution.
+#
+
+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"
+}

File testman4trac/trunk/testmanager/macros.py

View file
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010 Roberto Longobardi, Marco Cipriani
+#
+
+from genshi.builder import tag
+
+from trac.core import *
+from trac.wiki.macros import WikiMacroBase
+from trac.wiki.api import WikiSystem, parse_args
+from trac.wiki.model import WikiPage
+
+from tracgenericclass.util import *
+
+from testmanager.labels import *
+from testmanager.model import TestCatalog, TestCase, TestCaseInPlan, TestPlan
+from testmanager.util import *
+
+
+# Macros
+
+class TestCaseBreadcrumbMacro(WikiMacroBase):
+    """Display a breadcrumb with the path to the current catalog or test case.
+
+    Usage:
+
+    {{{
+    [[TestCaseBreadcrumb()]]
+    }}}
+    """
+    
+    def expand_macro(self, formatter, name, content):
+        if not content:
+            content = formatter.resource.id
+        
+        req = formatter.req
+
+        return _build_testcases_breadcrumb(self.env, req, content)
+
+        
+
+class TestCaseTreeMacro(WikiMacroBase):
+    """Display a tree with catalogs and test cases.
+
+    Usage:
+
+    {{{
+    [[TestCaseTree()]]
+    }}}
+    """
+    
+    def expand_macro(self, formatter, name, content):
+        if not content:
+            content = formatter.resource.id
+        
+        req = formatter.req
+
+        return _build_catalog_tree(self.env, req, content)
+
+
+class TestPlanTreeMacro(WikiMacroBase):
+    """Display a tree with catalogs and test cases in a test plan. 
+       Includes test case status in the plan.
+
+    Usage:
+
+    {{{
+    [[TestPlanTree(planid=<Plan ID>, catalog_path=<Catalog path>)]]
+    }}}
+    """
+    
+    def expand_macro(self, formatter, name, content):
+        args, kw = parse_args(content)
+
+        planid = kw.get('planid', -1)
+        catpath = kw.get('catalog_path', 'TC')
+        
+        req = formatter.req
+
+        return _build_testplan_tree(self.env, req, planid, catpath)
+
+
+class TestPlanListMacro(WikiMacroBase):
+    """Display a list of all the plans available for a test catalog.
+
+    Usage:
+
+    {{{
+    [[TestPlanListMacro(catalog_path=<Catalog path>)]]
+    }}}
+    """
+    
+    def expand_macro(self, formatter, name, content):
+        args, kw = parse_args(content)
+
+        catpath = kw.get('catalog_path', 'TC')
+        
+        req = formatter.req
+
+        return _build_testplan_list(self.env, req, catpath)
+
+        
+class TestCaseStatusMacro(WikiMacroBase):
+    """Display a colored icon according to the test case status in the specified test plan.
+
+    Usage:
+
+    {{{
+    [[TestCaseStatus(planid=<Plan ID>)]]
+    }}}
+    """
+    
+    def expand_macro(self, formatter, name, content):
+        args, kw = parse_args(content)
+
+        planid = kw.get('planid', -1)
+        curpage = kw.get('page_name', 'TC')
+        
+        req = formatter.req
+
+        return _build_testcase_status(self.env, req, planid, curpage)
+
+        
+class TestCaseChangeStatusMacro(WikiMacroBase):
+    """Display a semaphore to set the test case status in the specified test plan.
+
+    Usage:
+
+    {{{
+    [[TestCaseChangeStatus(planid=<Plan ID>)]]
+    }}}
+    """
+    
+    def expand_macro(self, formatter, name, content):
+        args, kw = parse_args(content)
+
+        planid = kw.get('planid', -1)
+        curpage = kw.get('page_name', 'TC')
+        
+        req = formatter.req
+
+        return _build_testcase_change_status(self.env, req, planid, curpage)
+
+        
+class TestCaseStatusHistoryMacro(WikiMacroBase):
+    """Display the history of status changes of a test case in the specified test plan.
+
+    Usage:
+
+    {{{
+    [[TestCaseStatusHistory(planid=<Plan ID>)]]
+    }}}
+    """
+    
+    def expand_macro(self, formatter, name, content):
+        args, kw = parse_args(content)
+
+        planid = kw.get('planid', -1)
+        curpage = kw.get('page_name', 'TC')
+        
+        req = formatter.req
+
+        return _build_testcase_status_history(self.env, req, planid, curpage)
+
+        
+
+# Internal methods
+
+def _build_testcases_breadcrumb(env,req,curpage):
+    # Determine current catalog name
+    cat_name = 'TC'
+    if curpage.find('_TC') >= 0:
+        cat_name = curpage.rpartition('_TC')[0].rpartition('_')[2]
+    elif not curpage == 'TC':
+        cat_name = curpage.rpartition('_')[2]
+    
+    # Create the breadcrumb model
+    path_name = curpage.partition('TC_')[2]
+    tokens = path_name.split("_")
+    curr_path = 'TC'
+    
+    breadcrumb = [{'name': 'TC', 'title': LABELS['all_catalogs'], 'id': 'TC'}]
+
+    for i, tc in enumerate(tokens):
+        curr_path += '_'+tc
+        page = WikiPage(env, curr_path)
+        page_title = get_page_title(page.text)
+        
+        breadcrumb[(i+1):] = [{'name': tc, 'title': page_title, 'id': curr_path}]
+
+        if tc == cat_name:
+            break
+
+    text = ''
+
+    text +='<div>'
+    text += _render_breadcrumb(breadcrumb)
+    text +='</div>'
+
+    return text    
+            
+
+def _build_catalog_tree(env,req,curpage):
+    # Determine current catalog name
+    cat_name = 'TC'
+    if curpage.find('_TC') >= 0:
+        cat_name = curpage.rpartition('_TC')[0].rpartition('_')[2]
+    elif not curpage == 'TC':
+        cat_name = curpage.rpartition('_')[2]
+    # Create the catalog subtree model
+    components = {'name': curpage, 'childrenC': {},'childrenT': {}, 'tot': 0}
+
+    for subpage_name in sorted(WikiSystem(env).get_pages(curpage+'_')):
+        subpage = WikiPage(env, subpage_name)
+        subpage_title = get_page_title(subpage.text)
+
+        path_name = subpage_name.partition(curpage+'_')[2]
+        tokens = path_name.split("_")
+        parent = components
+        ltok = len(tokens)
+        count = 1
+        curr_path = curpage
+        for tc in tokens:
+            curr_path += '_'+tc
+            
+            if tc == '':
+                break
+
+            if not tc.startswith('TC'):
+                comp = {}
+                if (tc not in parent['childrenC']):
+                    comp = {'id': curr_path, 'name': tc, 'title': subpage_title, 'childrenC': {},'childrenT': {}, 'tot': 0, 'parent': parent}
+                    parent['childrenC'][tc]=comp
+                else:
+                    comp = parent['childrenC'][tc]
+                parent = comp
+
+            else:
+                # It is a test case page
+                parent['childrenT'][tc]={'id':curr_path, 'title': subpage_title, 'status': 'NONE'}
+                compLoop = parent
+                while (True):
+                    compLoop['tot']+=1
+                    if ('parent' in compLoop):
+                        compLoop = compLoop['parent']
+                    else:
+                        break
+            count+=1
+
+    # Generate the markup
+    ind = {'count': 0}
+    text = ''
+
+    text +='<div style="padding: 0px 0px 10px 10px">'+LABELS['filter_label']+' <input id="tcFilter" title="'+LABELS['filter_help']+'" type="text" size="40" onkeyup="starthighlight(this.value)"/>&nbsp;&nbsp;<span id="searchResultsNumberId" style="font-weight: bold;"></span></div>'
+    text +='<div style="font-size: 0.8em;padding-left: 10px"><a style="margin-right: 10px" onclick="toggleAll(true)" href="javascript:void(0)">'+LABELS['expand_all']+'</a><a onclick="toggleAll(false)" href="javascript:void(0)">'+LABELS['collapse_all']+'</a></div>';
+    text +='<div id="ticketContainer">'
+
+    text += _render_subtree(-1, components, ind, 0)
+    
+    text +='</div>'
+    return text
+    
+def _build_testplan_tree(env, req, planid, curpage):
+    # Determine current catalog name
+    cat_name = 'TC'
+    if curpage.find('_TC') >= 0:
+        cat_name = curpage.rpartition('_TC')[0].rpartition('_')[2]
+    elif not curpage == 'TC':
+        cat_name = curpage.rpartition('_')[2]
+    # Create the catalog subtree model
+    components = {'name': curpage, 'childrenC': {},'childrenT': {}, 'tot': 0}
+
+    for subpage_name in sorted(WikiSystem(env).get_pages(curpage+'_')):
+        subpage = WikiPage(env, subpage_name)
+        subpage_title = get_page_title(subpage.text)
+
+        path_name = subpage_name.partition(curpage+'_')[2]
+        tokens = path_name.split("_")
+        parent = components
+        ltok = len(tokens)