Commits

seccanj committed b0136d2

Release 1.3.7. Enhancement #7704 (Track-Hacks): Add ability to delete a Test Plan, Fixet Ticket #8084 (Track-Hacks): Ordering issue

Comments (0)

Files changed (65)

build.sh

File contents unchanged.

clean.sh

File contents unchanged.

package.sh

File contents unchanged.

sqlexecutor/trunk/setup.py

 
 setup(
     name='SQLExecutor',
-    version='1.0.0',
+    version='1.0.1',
     packages=['sqlexecutor'],
     package_data={'sqlexecutor' : ['*.txt', 'templates/*.html', 'htdocs/*.*', 'htdocs/js/*.js', 'htdocs/css/*.css', 'htdocs/images/*.*']},
     author = 'Roberto Longobardi',

sqlexecutor/trunk/sqlexecutor/LICENSE.txt

File contents unchanged.

sqlexecutor/trunk/sqlexecutor/README.txt

File contents unchanged.

sqlexecutor/trunk/sqlexecutor/__init__.py

File contents unchanged.

sqlexecutor/trunk/sqlexecutor/htdocs/place.holder

File contents unchanged.

sqlexecutor/trunk/sqlexecutor/sql.py

 from datetime import datetime
 from trac.core import *
 from trac.perm import IPermissionRequestor
+from trac.util.text import CRLF
 from trac.util.translation import _, N_, gettext
 from trac.web.api import IRequestHandler
 from trac.web.chrome import ITemplateProvider
             for row in cursor:
                 for i in row:
                     result += str(i) + ', '
+                result += CRLF
 
             db.commit()
             
         from pkg_resources import resource_filename
         return [('sqlexecutor', resource_filename(__name__, 'htdocs'))]
 
-       
+       

sqlexecutor/trunk/sqlexecutor/templates/empty.html

File contents unchanged.

sqlexecutor/trunk/sqlexecutor/templates/result.html

File contents unchanged.

testman4trac/trunk/setup.py

 
 setup(
     name='TestManager',
-    version='1.3.6',
+    version='1.3.7',
     packages=['testmanager'],
     package_data={'testmanager' : ['*.txt', 'templates/*.html', 'htdocs/js/*.js', 'htdocs/css/*.css', 'htdocs/images/*.*']},
-    author = 'Roberto Longobardi, Marco Cipriani',
+    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',

testman4trac/trunk/testmanager/INSTALLATION.txt

File contents unchanged.

testman4trac/trunk/testmanager/LICENSE.txt

File contents unchanged.

testman4trac/trunk/testmanager/README.txt

   
   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.)
+(Refer to the tickets on trac-hacks or SourceForge for complete descriptions.)
+
+Release 1.3.7 (2010-11-20):
+  o Enhancement #7704 (Track-Hacks): Add ability to delete a Test Plan
+  o Fixet Ticket #8084 (Track-Hacks): Ordering issue
+
+Release 1.3.6 (2010-11-09):
+  o Fixed Ticket #8004 (Track-Hacks): Cannot search if an admin
+
+Release 1.3.5 (2010-10-17):
+  o Restored compatibility with Trac 0.11. Now again both 0.11 and 0.12 are supported.
+
+Release 1.3.4 (2010-10-15):
+  o Added tabular view to catalogs and plans. Search now works with custom properties in tabular views.
 
 Release 1.3.3 (2010-10-05):
   o Enhanced feature 3076739 (SourceForge): Full Test Plan display / print
           (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
+  o Enhancement #7704 (Track-Hacks): 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.
         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
+  o Enhancement #7705 (Track-Hacks): 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.
         good_prop = text
         good_prop.value = linux
 
-  o Enhancement #7569 Add listener interface to let other components react to test case status change
+  o Enhancement #7569 (Track-Hacks): 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)
       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
+  o Enhancement #7552 (Track-Hacks): Export test statistics in CSV and bookmark this chart features in the test stats chart
+  o Fixed Ticket #7551 (Track-Hacks): 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 Enhancement #7526 (Track-Hacks): Add ability to duplicate a test case
+  o Enhancement #7536 (Track-Hacks): 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
+  o Enhancement #7487 (Track-Hacks): Add multiple test plans capability
+  o Enhancement #7507 (Track-Hacks): Implement security permissions
+  o Enhancement #7484 (Track-Hacks): 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
+  o Fixed Ticket #7485 (Track-Hacks): "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

testman4trac/trunk/testmanager/__init__.py

File contents unchanged.

testman4trac/trunk/testmanager/api.py

             match = True
         elif (req.path_info.startswith('/teststatusupdate') and 'TEST_EXECUTE' in req.perm):
             match = True
+        elif (req.path_info.startswith('/testdelete') and (type == 'testplan' and ('TEST_PLAN_ADMIN' in req.perm))):
+            match = True
         
         return match
 
             
             if type == 'catalog':
                 req.perm.require('TEST_MODIFY')
-                pagename += '_TT'+str(id)
 
                 try:
                     new_tc = TestCatalog(self.env, id, pagename, title, '')
                     new_tc.insert()
                     
                 except:
-                    print "Error adding test catalog!"
-                    print formatExceptionInfo()
+                    self.env.log.info("Error adding test catalog!")
+                    self.env.log.info(formatExceptionInfo())
                     req.redirect(req.href.wiki(path))
 
                 # Redirect to see the new wiki page.
                     new_tc.insert()
 
                 except:
-                    print "Error adding test plan!"
-                    print formatExceptionInfo()
+                    self.env.log.info("Error adding test plan!")
+                    self.env.log.info(formatExceptionInfo())
                     # Back to the catalog
                     req.redirect(req.href.wiki(path))
 
                         else:
                             self.env.log.debug("Test case not found")
                     except:
-                        self.env.log.debug("Error pasting test case!")
-                        self.env.log.debug(formatExceptionInfo())
+                        self.env.log.info("Error pasting test case!")
+                        self.env.log.info(formatExceptionInfo())
                         req.redirect(req.href.wiki(pagename))
                 
                     # Redirect to test catalog, forcing a page refresh by means of a random request parameter
                         new_tc.save_as({'id': id})
                         
                     except:
-                        self.env.log.debug("Error duplicating test case!")
-                        self.env.log.debug(formatExceptionInfo())
+                        self.env.log.info("Error duplicating test case!")
+                        self.env.log.info(formatExceptionInfo())
                         req.redirect(req.href.wiki(tcId))
 
                     # Redirect tp allow for editing the copy test case
                         new_tc.insert()
                         
                     except:
-                        self.env.log.debug("Error adding test case!")
-                        self.env.log.debug(formatExceptionInfo())
+                        self.env.log.info("Error adding test case!")
+                        self.env.log.info(formatExceptionInfo())
                         req.redirect(req.path_info)
 
                     # Redirect to edit the test case description
                     req.redirect(req.href.wiki(pagename, action='edit'))
+
+        elif req.path_info.startswith('/testdelete'):
+            type = req.args.get('type')
+            path = req.args.get('path')
+            author = get_reporter_id(req, 'author')
+            mode = req.args.get('mode', 'tree')
+            fulldetails = req.args.get('fulldetails', 'False')
+
+            if type == 'testplan':
+                req.perm.require('TEST_PLAN_ADMIN')
+                
+                planid = req.args.get('planid')
+                catid = path.rpartition('_TT')[2]
+
+                self.env.log.debug("About to delete test plan %s on catalog %s" % (planid, catid))
+
+                try:
+                    # Add the new test plan in the database
+                    tp = TestPlan(self.env, planid, catid)
+                    tp.delete()
+
+                except:
+                    self.env.log.info("Error deleting test plan!")
+                    self.env.log.info(formatExceptionInfo())
+                    # Back to the catalog
+                    req.redirect(req.href.wiki(path))
+
+                # Redirect to test catalog, forcing a page refresh by means of a random request parameter
+                req.redirect(req.href.wiki(path, mode=mode, fulldetails=fulldetails, random=str(datetime.now(utc).microsecond)))
+                    
         
         return 'empty.html', {}, None
 

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

File contents unchanged.

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

Old
Old image
New
New image

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

Old
Old image
New
New image

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

Old
Old image
New
New image

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

Old
Old image
New
New image

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

Old
Old image
New
New image

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

Old
Old image
New
New image

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

Old
Old image
New
New image

testman4trac/trunk/testmanager/htdocs/images/trash.png

Added
New image

testman4trac/trunk/testmanager/htdocs/images/tree.png

Old
Old image
New
New image

testman4trac/trunk/testmanager/htdocs/images/tree_table.png

Old
Old image
New
New image

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

Old
Old image
New
New image

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

File contents unchanged.

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

 /*- coding: utf-8
  *
- * Copyright (C) 2010 Roberto Longopbardi - seccanj@gmail.com, Marco Cipriani
+ * Copyright (C) 2010 Roberto Longobardi - seccanj@gmail.com
  */
 
 var messages = {
     'name_help': "You must specify a name. Length between 4 and 90 characters.",
-    'length_error': "Length between 4 and 90 characters."
+    'length_error': "Length between 4 and 90 characters.",
+    'duplicate_catalog_confirm': "Are you sure you want to duplicate the test catalog and all its contained test cases?",
+    'delete_plan_confirm': "Are you sure you want to delete the test plan and the state of all its contained test cases?"
 };
 
 var labels = {

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

 /*- coding: utf-8
  *
- * Copyright (C) 2010 Roberto Longopbardi - seccanj@gmail.com, Marco Cipriani
+ * Copyright (C) 2010 Roberto Longobardi - seccanj@gmail.com
  */
 
 var messages = {
     'name_help': "You must specify a name. Length between 4 and 90 characters.",
-    'length_error': "Length between 4 and 90 characters."
+    'length_error': "Length between 4 and 90 characters.",
+    'duplicate_catalog_confirm': "Are you sure you want to duplicate the test catalog and all its contained test cases?",
+    'delete_plan_confirm': "Are you sure you want to delete the test plan and the state of all its contained test cases?"
 };
 
 var labels = {

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

 /*- coding: utf-8
  *
- * Copyright (C) 2010 Roberto Longopbardi - seccanj@gmail.com, Marco Cipriani
+ * Copyright (C) 2010 Roberto Longobardi - seccanj@gmail.com
  */
 
 var messages = {
     'name_help': "Devi indicare un nome. Lunghezza da 4 a 90 caratteri.",
-    'length_error': "Lunghezza da 4 a 90 caratteri."
+    'length_error': "Lunghezza da 4 a 90 caratteri.",
+    'duplicate_catalog_confirm': "Sei sicuro di voler duplicare il catalogo e tutti i test case in esso contenuti?",
+    'delete_plan_confirm': "Sei sicuro di voler eliminare il piano di test e lo stato di tutti i test case in esso contenuti?"
 };
 
 var labels = {

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

 /*- coding: utf-8
  *
- * Copyright (C) 2010 Roberto Longopbardi - seccanj@gmail.com, Marco Cipriani
+ * Copyright (C) 2010 Roberto Longobardi - seccanj@gmail.com
  */
 
 /******************************************************/
 	window.location = url;
 }
 
+function duplicateTestCatalog(catName){
+    if (confirm(messages['duplicate_catalog_confirm'])) {
+        var url = baseLocation+'/testcreate?type=catalog&duplicate=true&path='+catName; 
+        window.location = url;
+    }
+}
+
+function deleteTestPlan(url){
+    if (confirm(messages['delete_plan_confirm'])) {
+        window.location = url;
+    }
+}
+
 /******************************************************/
 /**         Move test case into another catalog       */
 /******************************************************/

testman4trac/trunk/testmanager/labels.py

     'duplicate_tc_button': "Duplicate the Test Case",
     'edit_test_case_label': "Edit the Test Case",
     'edit_label': "Edit",
-    'update_button': "Save"
+    'update_button': "Save",
+    'name_header': "Name",
+    'id_header': "ID",
+    'description_header': "Description",
+    'last_change_header': "Last Change",
+    'delete': "Delete",
+    'duplicate_catalog': "Duplicate the Catalog"
 }

testman4trac/trunk/testmanager/labels_en.py

     'duplicate_tc_button': "Duplicate the Test Case",
     'edit_test_case_label': "Edit the Test Case",
     'edit_label': "Edit",
-    'update_button': "Save"
+    'update_button': "Save",
+    'name_header': "Name",
+    'id_header': "ID",
+    'description_header': "Description",
+    'last_change_header': "Last Change",
+    'delete': "Delete",
+    'duplicate_catalog': "Duplicate the Catalog"
 }

testman4trac/trunk/testmanager/labels_it.py

     '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",
+    'test_case': "Caso 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:",
     'duplicate_tc_button': "Duplica il Caso di Test",
     'edit_test_case_label': "Modifica il Caso di Test",
     'edit_label': "Modifica",
-    'update_button': "Salva"
+    'update_button': "Salva",
+    'name_header': "Nome",
+    'id_header': "ID",
+    'description_header': "Descrizione",
+    'last_change_header': "Ultima Modifica",
+    'delete': "Elimina",
+    'duplicate_catalog': "Duplica il Catalogo"
 }

testman4trac/trunk/testmanager/macros.py

         args, kw = parse_args(content)
 
         page_name = kw.get('page_name', 'TC')
+        planid = kw.get('planid', '-1')
         mode = kw.get('mode', 'tree')
         fulldetails = kw.get('fulldetails', 'False')
         
         
         req = formatter.req
 
-        return _build_testcases_breadcrumb(self.env, req, page_name, mode, fulldetails)
+        return _build_testcases_breadcrumb(self.env, req, page_name, planid, mode, fulldetails)
         
 
 class TestCaseTreeMacro(WikiMacroBase):
     Usage:
 
     {{{
-    [[TestPlanTree(planid=<Plan ID>, catalog_path=<Catalog path>, mode={'tree'|'tree_table'})]]
+    [[TestPlanTree(planid=<Plan ID>, catalog_path=<Catalog path>, mode={'tree'|'tree_table'}, sortby={'modification_time'|'name'})]]
     }}}
     """
     
     def expand_macro(self, formatter, name, content):
         args, kw = parse_args(content)
 
-        planid = kw.get('planid', -1)
+        planid = kw.get('planid', '-1')
         catpath = kw.get('catalog_path', 'TC')
         mode = kw.get('mode', 'tree')
+        sortby = kw.get('sortby', 'name')
         
         req = formatter.req
 
-        return _build_testplan_tree(self.env, req, formatter.context, planid, catpath, mode)
+        return _build_testplan_tree(self.env, req, formatter.context, planid, catpath, mode, sortby)
 
 
 class TestPlanListMacro(WikiMacroBase):
         args, kw = parse_args(content)
 
         catpath = kw.get('catalog_path', 'TC')
+        mode = kw.get('mode', 'tree')
+        fulldetails = kw.get('fulldetails', 'False')
+        
+        fulldetails = (fulldetails == 'True')
         
         req = formatter.req
 
-        return _build_testplan_list(self.env, req, catpath)
+        return _build_testplan_list(self.env, req, catpath, mode, fulldetails)
 
         
 class TestCaseStatusMacro(WikiMacroBase):
     def expand_macro(self, formatter, name, content):
         args, kw = parse_args(content)
 
-        planid = kw.get('planid', -1)
+        planid = kw.get('planid', '-1')
         curpage = kw.get('page_name', 'TC')
         
         req = formatter.req
     def expand_macro(self, formatter, name, content):
         args, kw = parse_args(content)
 
-        planid = kw.get('planid', -1)
+        planid = kw.get('planid', '-1')
         curpage = kw.get('page_name', 'TC')
         
         req = formatter.req
     def expand_macro(self, formatter, name, content):
         args, kw = parse_args(content)
 
-        planid = kw.get('planid', -1)
+        planid = kw.get('planid', '-1')
         curpage = kw.get('page_name', 'TC')
         
         req = formatter.req
 
 # Internal methods
 
-def _build_testcases_breadcrumb(env,req,curpage, mode, fulldetails):
+def _build_testcases_breadcrumb(env, req, curpage, planid, mode, fulldetails):
     # Determine current catalog name
     cat_name = 'TC'
     if curpage.find('_TC') >= 0:
     text = ''
 
     text +='<div>'
-    text += _render_breadcrumb(breadcrumb, mode, fulldetails)
+    text += _render_breadcrumb(breadcrumb, planid, mode, fulldetails)
     text +='</div>'
 
     print text
     # Create the catalog subtree model
     components = {'name': curpage, 'childrenC': {},'childrenT': {}, 'tot': 0}
 
+    unique_idx = 0
+
     for subpage_name in sorted(WikiSystem(env).get_pages(curpage+'_')):
         subpage = WikiPage(env, subpage_name)
         subpage_title = get_page_title(subpage.text)
                 # It is a test case page
                 tc_id = tc.rpartition('TC')[2]
                 
-                parent['childrenT'][tc]={'id':curr_path, 'tc_id':tc_id, 'title': subpage_title, 'status': 'NONE'}
+                if subpage_title in parent['childrenT']:
+                    unique_idx += 1
+                    key = subpage_title+str(unique_idx)
+                else:
+                    key = subpage_title
+                    
+                parent['childrenT'][key]={'id':curr_path, 'tc_id':tc_id, 'title': subpage_title, 'status': 'NONE'}
                 compLoop = parent
                 while (True):
                     compLoop['tot']+=1
         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 += _render_subtree('-1', components, ind, 0)
         
         text +='</div>'
         
         text += '<table id="testcaseList" class="listing"><thead><tr>';
         
         # Common columns
-        text += '<th>'+"Name"+'</th>'
+        text += '<th>'+LABELS['name_header']+'</th>'
         
         # Custom testcatalog columns
         if tcat_has_custom:
                     text += '<th>'+f['label']+'</th>'
 
         # Base testcase columns
-        text += '<th>'+"ID"+'</th>'
+        text += '<th>'+LABELS['id_header']+'</th>'
 
         # Custom testcase columns
         if tc_has_custom:
         
         # Test case full details
         if fulldetails:
-            text += '<th>'+"Description"+'</th>'
+            text += '<th>'+LABELS['description_header']+'</th>'
             
         text += '</tr></thead><tbody>';
         
     
     return text
     
-def _build_testplan_tree(env, req, context, planid, curpage, mode='tree'):
+def _build_testplan_tree(env, req, context, planid, curpage, mode='tree',sortby='name'):
     # Determine current catalog name
     cat_name = 'TC'
     if curpage.find('_TC') >= 0:
     # Create the catalog subtree model
     components = {'name': curpage, 'childrenC': {},'childrenT': {}, 'tot': 0}
 
+    unique_idx = 0
+
     for subpage_name in sorted(WikiSystem(env).get_pages(curpage+'_')):
         subpage = WikiPage(env, subpage_name)
         subpage_title = get_page_title(subpage.text)
                     ts = tp['time']
                     author = tp['author']
                     status = 'TO_BE_TESTED'
+                
+                if sortby == 'name':
+                    key = subpage_title
+                else:
+                    key = ts.isoformat()
+
+                if key in parent['childrenT']:
+                    unique_idx += 1
+                    key = key+str(unique_idx)
                     
-                parent['childrenT'][tc]={'id':curr_path, 'tc_id': tc_id, 'title': subpage_title, 'status': status, 'ts': ts, 'author': author}
+                parent['childrenT'][key]={'id':curr_path, 'tc_id': tc_id, 'title': subpage_title, 'status': status, 'ts': ts, 'author': author}
                 compLoop = parent
                 while (True):
                     compLoop['tot']+=1
         text += '<table id="testcaseList" class="listing"><thead><tr>';
 
         # Common columns
-        text += '<th>'+"Name"+'</th>'
+        text += '<th>'+LABELS['name_header']+'</th>'
         
         # Custom testcatalog columns
         if custom_ctx['testcatalog'][0]:
                     text += '<th>'+f['label']+'</th>'
 
         # Base testcase columns
-        text += '<th>'+"ID"+'</th>'
+        text += '<th>'+LABELS['id_header']+'</th>'
 
         #Custom testcase columns
         if custom_ctx['testcase'][0]:
                     text += '<th>'+f['label']+'</th>'
 
         # Base testcaseinplan columns
-        text += '<th>'+"Status"+'</th><th>'+"Author"+'</th><th>'+"Last Change"+'</th>'
+        text += '<th>'+LABELS['status']+'</th><th>'+LABELS['author']+'</th><th>'+LABELS['last_change_header']+'</th>'
         
         # Custom testcaseinplan columns
         if custom_ctx['testcaseinplan'][0]:
     return text
 
 
-def _build_testplan_list(env, req, curpage):
+def _build_testplan_list(env, req, curpage, mode, fulldetails):
     # Determine current catalog name
     cat_name = 'TC'
-    catid = -1
+    catid = '-1'
     if curpage.find('_TC') >= 0:
         cat_name = curpage.rpartition('_TC')[0].rpartition('_')[2]
         catid = cat_name.rpartition('TT')[2]
     elif not curpage == 'TC':
         cat_name = curpage.rpartition('_')[2]
         catid = cat_name.rpartition('TT')[2]
-        
-    markup, num_plans = _render_testplan_list(env, catid)
-        
+    
+    if 'TEST_PLAN_ADMIN' in req.perm:
+        show_delete_button = True
+    else:
+        show_delete_button = False
+    
+    markup, num_plans = _render_testplan_list(env, catid, mode, fulldetails, show_delete_button)
+
     text = '<form id="testPlanList"><fieldset id="testPlanListFields" class="collapsed"><legend class="foldable" style="cursor: pointer;"><a href="#no4"  onclick="expandCollapseSection(\'testPlanListFields\')">'+LABELS['test_plan_list']+' ('+str(num_plans)+')</a></legend>'
     text += markup
     text += '</fieldset></form>'
 
     return text
     
-def _render_testplan_list(env, catid):
+def _render_testplan_list(env, catid, mode, fulldetails, show_delete_button):
     """Returns a test case status in a plan audit trail."""
 
+    delete_icon = '../chrome/testmanager/images/trash.png'
+
     cat = TestCatalog(env, catid)
     
     result = '<table class="listing"><thead>'
-    result += '<tr><th>'+LABELS['plan_name']+'</th><th>'+LABELS['author']+'</th><th>'+LABELS['timestamp']+'</th></tr>'
+    result += '<tr><th>'+LABELS['plan_name']+'</th><th>'+LABELS['author']+'</th><th>'+LABELS['timestamp']+'</th><th></th></tr>'
     result += '</thead><tbody>'
     
     num_plans = 0
-    for tp in cat.list_testplans():
+    for tp in sorted(cat.list_testplans(), cmp=lambda x,y: cmp(x['time'],y['time']), reverse=True):
         result += '<tr>'
         result += '<td><a title="'+LABELS['open_testplan_title']+'" href="'+tp['page_name']+'?planid='+tp['id']+'">'+tp['name']+'</a></td>'
         result += '<td>'+tp['author']+'</td>'
         result += '<td>'+str(tp['time'])+'</td>'
+        
+        if show_delete_button:
+            result += '<td style="cursor: pointer;"><img class="iconElement" alt="'+LABELS['delete']+'" title="'+LABELS['delete']+'" src="'+delete_icon+'" onclick="deleteTestPlan(\'../testdelete?type=testplan&path='+tp['page_name']+'&mode='+mode+'&fulldetails='+str(fulldetails)+'&planid='+tp['id']+'\')"/></td>'
+        else:
+            result += '<td></td>'
+        
         result += '</tr>'
         num_plans += 1
 
     return result, num_plans
     
 # Render the breadcrumb
-def _render_breadcrumb(breadcrumb, mode, fulldetails):
+def _render_breadcrumb(breadcrumb, planid, mode, fulldetails):
+    plan_ref = ''
+    if planid is not None and not planid == '-1':
+        plan_ref = '&planid='+planid
+    
     text = ''
     path_len = len(breadcrumb)
     for i, x in enumerate(breadcrumb):
-        text += '<span name="breadcrumb" style="cursor: pointer; color: #BB0000; margin-left: 10px; margin-right: 10px; font-size: 0.8em;" '
-        text += ' onclick="window.location=\''+x['id']+'?mode='+mode+'&fulldetails='+str(fulldetails)+'\'">'+x['title']
+        if i == 0:
+            plan_param = ''
+        else:
+            plan_param = plan_ref
+    
+        text += '<span name="breadcrumb" style="cursor: pointer; color: #BB0000; margin-left: 10px; margin-right: 5px; font-size: 0.8em;" '
+        text += ' onclick="window.location=\''+x['id']+'?mode='+mode+plan_param+'&fulldetails='+str(fulldetails)+'\'">'+x['title']
         
         if i < path_len-1:
             text += '&nbsp;&nbsp;->'
     text = ''
     if (level == 0):
         data = component['childrenC']
-        text +='<ul style="list-style: none;">';        
+        text +='<ul style="list-style: none;">';
     keyList = data.keys()
     sortedList = sorted(keyList)
     for x in sortedList:
                 toggle_icon = '../chrome/testmanager/images/empty.png'
                 
             index = str(ind['count'])
-            
-            text+='<span name="'+toggable+'" style="cursor: pointer" id="b_'+index+'"><span onclick="toggle(\'b_'+index+'\')"><img class="iconElement" src="'+toggle_icon+'" /></span><span id="l_'+index+'" onmouseover="underlineLink(\'l_'+index+'\')" onmouseout="removeUnderlineLink(\'l_'+index+'\')" onclick="window.location=\''+comp['id']+'\'" title='+LABELS['open']+'>'+comp['title']+'</span></span><span style="color: gray;">&nbsp;('+str(comp['tot'])+')</span>'
+            if planid is not None and not planid == '-1':
+                plan_param = '?planid='+planid
+            else:
+                plan_param = ''
+                
+            text+='<span name="'+toggable+'" style="cursor: pointer" id="b_'+index+'"><span onclick="toggle(\'b_'+index+'\')"><img class="iconElement" src="'+toggle_icon+'" /></span><span id="l_'+index+'" onmouseover="underlineLink(\'l_'+index+'\')" onmouseout="removeUnderlineLink(\'l_'+index+'\')" onclick="window.location=\''+comp['id']+plan_param+'\'" title='+LABELS['open']+'>'+comp['title']+'</span></span><span style="color: gray;">&nbsp;('+str(comp['tot'])+')</span>'
             text +='<ul id="b_'+index+'_list" style="display:none;list-style: none;">';
             ind['count']+=1
             text+=_render_subtree(planid, subcData, ind, level+1)
 
         if has_status:
             statusLabel = LABELS[status]
-            text+="<li style='font-weight: normal;' onmouseover='showPencil(\"pencilIcon"+tick['id']+"\", true)' onmouseout='hidePencil(\"pencilIcon"+tick['id']+"\", false)'><img class='iconElement' src='"+statusIcon+"' title='"+statusLabel+"'></img><a href='"+tick['id']+"?planid="+str(planid)+"' target='_blank'>"+tick['title']+"&nbsp;</a><span style='display: none;'>"+statusLabel+"</span><span><a class='rightIcon' style='display: none;' title='"+LABELS['edit_test_case_label']+"' href='"+tick['id']+"?action=edit&planid="+str(planid)+"' target='_blank' id='pencilIcon"+tick['id']+"'></a></span></li>"
+            text+="<li style='font-weight: normal;' onmouseover='showPencil(\"pencilIcon"+tick['id']+"\", true)' onmouseout='hidePencil(\"pencilIcon"+tick['id']+"\", false)'><img class='iconElement' src='"+statusIcon+"' title='"+statusLabel+"'></img><a href='"+tick['id']+"?planid="+planid+"' target='_blank'>"+tick['title']+"&nbsp;</a><span style='display: none;'>"+statusLabel+"</span><span><a class='rightIcon' style='display: none;' title='"+LABELS['edit_test_case_label']+"' href='"+tick['id']+"?action=edit&planid="+planid+"' target='_blank' id='pencilIcon"+tick['id']+"'></a></span></li>"
         else:
             text+="<li style='font-weight: normal;' onmouseover='showPencil(\"pencilIcon"+tick['id']+"\", true)' onmouseout='hidePencil(\"pencilIcon"+tick['id']+"\", false)'><a href='"+tick['id']+"' target='_blank'>"+tick['title']+"&nbsp;</a><span><a class='rightIcon' style='display: none;' title='"+LABELS['edit_test_case_label']+"' href='"+tick['id']+"?action=edit' target='_blank' id='pencilIcon"+tick['id']+"'></a></span></li>"
             
             subcData=comp['childrenC']
             
             index = str(ind['count'])
+            if planid is not None and not planid == '-1':
+                plan_param = '&planid='+planid
+            else:
+                plan_param = ''
             
             # Common columns
-            text += '<tr name="testcatalog"><td style="padding-left: '+str(level*30)+'px;"><a href="'+comp['id']+'?mode=tree_table&fulldetails='+str(fulldetails)+'" title="'+LABELS['open']+'">'+comp['title']+'</a></td>'
+            text += '<tr name="testcatalog"><td style="padding-left: '+str(level*30)+'px;"><a href="'+comp['id']+'?mode=tree_table'+plan_param+'&fulldetails='+str(fulldetails)+'" title="'+LABELS['open']+'">'+comp['title']+'</a></td>'
 
             # Custom testcatalog columns
             if custom_ctx['testcatalog'][0]:
     if status == 'SUCCESSFUL':
         border = 'border: 2px solid black;'
 
-    text += '<span id="tcStatusSUCCESSFUL" style="padding: 3px; cursor: pointer;'+border+'" onclick="changestate(\''+tc_id+'\', \''+str(planid)+'\', \''+curpage+'\', \'SUCCESSFUL\')">'
+    text += '<span id="tcStatusSUCCESSFUL" style="padding: 3px; cursor: pointer;'+border+'" onclick="changestate(\''+tc_id+'\', \''+planid+'\', \''+curpage+'\', \'SUCCESSFUL\')">'
     text += '<img src="../chrome/testmanager/images/green.png" title="'+LABELS['SUCCESSFUL']+'"></img></span>'
 
     border = ''
     if status == 'TO_BE_TESTED':
         border = 'border: 2px solid black;'
 
-    text += '<span id="tcStatusTO_BE_TESTED" style="padding: 3px; cursor: pointer;'+border+'" onclick="changestate(\''+tc_id+'\', \''+str(planid)+'\', \''+curpage+'\', \'TO_BE_TESTED\')">'
+    text += '<span id="tcStatusTO_BE_TESTED" style="padding: 3px; cursor: pointer;'+border+'" onclick="changestate(\''+tc_id+'\', \''+planid+'\', \''+curpage+'\', \'TO_BE_TESTED\')">'
     text += '<img src="../chrome/testmanager/images/yellow.png" title="'+LABELS['TO_BE_TESTED']+'"></img></span>'
 
     border = ''
     if status == 'FAILED':
         border = 'border: 2px solid black;'
 
-    text += '<span id="tcStatusFAILED" style="padding: 3px; cursor: pointer;'+border+'" onclick="changestate(\''+tc_id+'\', \''+str(planid)+'\', \''+curpage+'\', \'FAILED\')">'
+    text += '<span id="tcStatusFAILED" style="padding: 3px; cursor: pointer;'+border+'" onclick="changestate(\''+tc_id+'\', \''+planid+'\', \''+curpage+'\', \'FAILED\')">'
     text += '<img src="../chrome/testmanager/images/red.png" title="'+LABELS['FAILED']+'"></img></span>'
 
     text += '</span>'

testman4trac/trunk/testmanager/model.py

         self.author = self.wikipage.author
 
         self.env.log.debug('Title: %s' % self.title)
-        self.env.log.debug('Description: %s' % self.description)
+        #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:
     
         AbstractTestDescription.__init__(self, env, 'testcatalog', id, page_name, title, description, db)
 
+    def get_enclosing_catalog(self):
+        """
+        Returns the catalog containing this test catalog, or None if its a root catalog.
+        """
+        page_name = self.values['page_name']
+        cat_page = page_name.rpartition('_TT')[0]
+
+        if cat_page == 'TC':
+            return None
+        else:
+            cat_id = page_name.rpartition('TT')[0].page_name.rpartition('TT')[2].rpartition('_')[0]
+
+            return TestCatalog(self.env, cat_id, cat_page)
+        
     def list_subcatalogs(self):
         """
         Returns a list of the sub catalogs of this catalog.
 
     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):
     
     def create_instance(self, key):
         return TestPlan(self.env, key['id'])
 
+    def post_delete(self, db):
+        self.env.log.debug("Deleting this test plan %s" % self['id'])
+        
+        # Remove all test cases (in plan) from this plan
+        self.env.log.debug("Deleting all test cases in the plan...")
+        tcip_search = TestCaseInPlan(self.env)
+        tcip_search['planid'] = self.values['id']
+        for tcip in tcip_search.list_matching_objects(db):
+            self.env.log.debug("Deleting test case in plan, with id %s" % tcip['id'])
+            tcip.delete(db)
+
+
         
 class TestManagerModelProvider(Component):
     """

testman4trac/trunk/testmanager/stats.py

File contents unchanged.

testman4trac/trunk/testmanager/templates/content.html

File contents unchanged.

testman4trac/trunk/testmanager/templates/empty.html

File contents unchanged.

testman4trac/trunk/testmanager/templates/jsondata.html

File contents unchanged.

testman4trac/trunk/testmanager/templates/testmanagerstats.html

File contents unchanged.

testman4trac/trunk/testmanager/util.py

File contents unchanged.

testman4trac/trunk/testmanager/web_ui.py

File contents unchanged.

testman4trac/trunk/testmanager/wiki.py

     
     implements(ITemplateStreamFilter, IWikiChangeListener)
     
+    _config_properties = {}
+    
+    def __init__(self, *args, **kwargs):
+        """
+        Parses the configuration file for the section 'testmanager'.
+        
+        Available properties are:
+        
+          testplan.sortby = {modification_time|name}    (default is name)
+        """
+        
+        Component.__init__(self, *args, **kwargs)
+
+        for section in self.config.sections():
+            if section == 'testmanager':
+                self.log.debug("WikiTestManagerInterface - parsing config section %s" % section)
+                options = list(self.config.options(section))
+                
+                self._parse_config_options(options)
+                break
+
+    
+    def _parse_config_options(self, options):
+        for option in options:
+            name = option[0]
+            value = option[1]
+            self.env.log.debug("  %s = %s" % (name, value))
+            
+            self._config_properties[name] = value
+    
     # IWikiChangeListener methods
     def wiki_page_added(self, page):
         #page_on_db = WikiPage(self.env, page.name)
             insert2.append(tag.div(id='pasteTCHereDiv')(
                         tag.input(type='button', id='pasteTCHereButton', value=LABELS['move_here'], onclick='pasteTestCaseIntoCatalog("'+cat_name+'")')
                     ))
-                    
+                                        
         insert2.append(tag.div(class_='field')(
                     tag.script('var baseLocation="'+req.href()+'";', type='text/javascript'),
                     tag.br(), tag.br(), tag.br(),
                             tag.input(type='button', value=LABELS['add_test_plan_button'], onclick='creaTestPlan("'+cat_name+'")')
                             ),
                         tag.br(), 
-                        self._get_testplan_list_markup(formatter, cat_name),
+                        self._get_testplan_list_markup(formatter, cat_name, mode, fulldetails),
                         ))
                     
         insert2.append(tag.div()(tag.br(), tag.br(), tag.br(), tag.br()))
         mode = req.args.get('mode', 'tree')
         fulldetails = req.args.get('fulldetails', 'False')
 
+        if 'testplan.sortby' in self._config_properties:
+            sortby = self._config_properties['testplan.sortby']
+        else:
+            sortby = 'name'
+
         tmmodelprovider = GenericClassModelProvider(self.env)
         test_plan = TestPlan(self.env, planid, cat_id, page_name)
         
                     )
 
         insert2 = tag.div()(
-                    HTML(tree_macro.expand_macro(formatter, None, 'planid='+str(planid)+',catalog_path='+page_name+',mode='+mode+',fulldetails='+fulldetails)),
+                    HTML(tree_macro.expand_macro(formatter, None, 'planid='+str(planid)+',catalog_path='+page_name+',mode='+mode+',fulldetails='+fulldetails+',sortby='+sortby)),
                     tag.div(class_='testCaseList')(
                     tag.br(), tag.br(),
                     self._get_custom_fields_markup(test_plan, tmmodelprovider.get_custom_fields_for_realm('testplan')),
         else:
             return tag.span()()
 
-    def _get_testplan_list_markup(self, formatter, cat_name):
+    def _get_testplan_list_markup(self, formatter, cat_name, mode, fulldetails):
         testplan_list_macro = TestPlanListMacro(self.env)
-        return HTML(testplan_list_macro.expand_macro(formatter, None, 'catalog_path='+cat_name))
+        return HTML(testplan_list_macro.expand_macro(formatter, None, 'catalog_path='+cat_name+',mode='+mode+',fulldetails='+str(fulldetails)))
 
     def _get_custom_fields_markup(self, obj, fields, props=None):
         obj_key = obj.gey_key_string()

testman4trac/trunk/testmanager/workflow.py

File contents unchanged.

tracgenericclass/trunk/setup.py

File contents unchanged.

tracgenericclass/trunk/tracgenericclass/LICENSE.txt

File contents unchanged.

tracgenericclass/trunk/tracgenericclass/README.txt

File contents unchanged.

tracgenericclass/trunk/tracgenericclass/__init__.py

File contents unchanged.

tracgenericclass/trunk/tracgenericclass/htdocs/place.holder

File contents unchanged.

tracgenericclass/trunk/tracgenericclass/model.py

File contents unchanged.

tracgenericclass/trunk/tracgenericclass/templates/empty.html

File contents unchanged.

tracgenericclass/trunk/tracgenericclass/util.py

         return (db, False)
 
     if not checked_compatibility:
-        check_compatibility()
+        check_compatibility(env)
 
     if has_read_db:
         return (env.get_read_db(), True)
 
     checked_compatibility = True
 
-
 def to_list(params=[]):
     result = []
     

tracgenericworkflow/trunk/setup.py

 
 setup(
     name='TracGenericWorkflow',
-    version='1.0.0',
+    version='1.0.1',
     packages=['tracgenericworkflow'],
     package_data={'tracgenericworkflow' : ['*.txt', 'templates/*.html', 'htdocs/*.*', 'htdocs/js/*.js', 'htdocs/css/*.css', 'htdocs/images/*.*']},
     author = 'Roberto Longobardi',

tracgenericworkflow/trunk/tracgenericworkflow/LICENSE.txt

File contents unchanged.

tracgenericworkflow/trunk/tracgenericworkflow/README.txt

File contents unchanged.

tracgenericworkflow/trunk/tracgenericworkflow/__init__.py

 
 import api
 import model
+import operations
+

tracgenericworkflow/trunk/tracgenericworkflow/api.py

                 for operation in operations:
                     provider = self.get_operation_provider(operation)
                     
-                    provider.perform_operation(req, selected_action, operation, curr_state, new_state, rws, res)
+                    if provider is not None:
+                        provider.perform_operation(req, selected_action, operation, curr_state, new_state, rws, res)
+                    else:
+                        self.env.log.debug("Unable to find operation provider for operation %s" % operation)
 
                 # Transition the resource to the new state
                 if rws.exists:

tracgenericworkflow/trunk/tracgenericworkflow/htdocs/place.holder

File contents unchanged.

tracgenericworkflow/trunk/tracgenericworkflow/model.py

File contents unchanged.

tracgenericworkflow/trunk/tracgenericworkflow/operations.py

+from trac.core import *
+from trac.resource import Resource
+from trac.util.datefmt import utc
+from trac.util.translation import _, N_, gettext
+
+from genshi.builder import tag
+from genshi.filters.transform import Transformer
+from genshi import HTML
+
+from tracgenericclass.util import *
+
+from tracgenericworkflow.model import ResourceWorkflowState
+from tracgenericworkflow.api import IWorkflowOperationProvider, ResourceWorkflowSystem
+
+
+# Out-of-the-box operations
+class WorkflowStandardOperations(Component):
+    """Adds a set of standard, out-of-the-box workflow operations."""
+    
+    implements(IWorkflowOperationProvider)
+
+    # IWorkflowOperationProvider methods
+    def get_implemented_operations(self):
+        self.log.debug(">>> WorkflowStandardOperations - get_implemented_operations")
+        self.log.debug("<<< WorkflowStandardOperations - get_implemented_operations")
+
+        yield 'set_owner'
+        yield 'set_owner_to_self'
+        yield 'std_notify'
+
+    def get_operation_control(self, req, action, operation, res_wf_state, resource):
+        self.log.debug(">>> WorkflowStandardOperations - get_operation_control: %s" % operation)
+
+        id = 'action_%s_operation_%s' % (action, operation)
+
+        # A custom field named "owner" is required in the ResourceWorkflowState 
+        # class for this operation to be available
+        
+        print (res_wf_state.fields)
+        
+        if operation == 'set_owner' and 'owner' in res_wf_state.fields:
+            self.log.debug("Creating control for setting owner.")
+
+            current_owner = res_wf_state['owner'] or '(none)'
+            if not (Chrome(self.env).show_email_addresses
+                    or 'EMAIL_VIEW' in req.perm(resource)):
+                format_user = obfuscate_email_address
+            else:
+                format_user = lambda address: address
+            current_owner = format_user(current_owner)
+
+            self.log.debug("Current owner is %s." % current_owner)
+
+            selected_owner = req.args.get(id, req.authname)
+
+            control = None
+            hint = ''
+
+            if self.config.getbool(resource.realm, 'set_owners'):
+                target_owners = self.config.getstring(resource.realm, 'restrict_to_permission')
+                owners = [x.strip() for x in
+                          target_owners.split(',')]
+            elif self.config.getbool(resource.realm, 'restrict_owner'):
+                target_permission = self.config.getstring(resource.realm, 'restrict_to_permission')
+                perm = PermissionSystem(self.env)
+                owners = perm.get_users_with_permission(target_permission)
+                owners.sort()
+            else:
+                owners = None
+
+            if owners == None:
+                owner = req.args.get(id, req.authname)
+                control = tag_('to %(owner)s',
+                                    owner=tag.input(type='text', id=id,
+                                                    name=id, value=owner))
+                hint = _("The owner will be changed from "
+                               "%(current_owner)s",
+                               current_owner=current_owner)
+            elif len(owners) == 1:
+                owner = tag.input(type='hidden', id=id, name=id,
+                                  value=owners[0])
+                formatted_owner = format_user(owners[0])
+                control = tag_('to %(owner)s ',
+                                    owner=tag(formatted_owner, owner))
+                if res_wf_state['owner'] != owners[0]:
+                    hint = _("The owner will be changed from "
+                                   "%(current_owner)s to %(selected_owner)s",
+                                   current_owner=current_owner,
+                                   selected_owner=formatted_owner)
+            else:
+                control = tag_('to %(owner)s', owner=tag.select(
+                    [tag.option(format_user(x), value=x,
+                                selected=(x == selected_owner or None))
+                     for x in owners],
+                    id=id, name=id))
+                hint = _("The owner will be changed from "
+                               "%(current_owner)s",
+                               current_owner=current_owner)
+
+            return control, hint
+
+        elif operation == 'set_owner_to_self' and 'owner' in res_wf_state.fields and \
+                res_wf_state['owner'] != req.authname:
+            hint = _("The owner will be changed from %(current_owner)s "
+                           "to %(authname)s", current_owner=current_owner,
+                           authname=req.authname)
+
+            self.log.debug("<<< WorkflowStandardOperations - get_operation_control - set_owner_to_self")
+            
+            return None, hint
+
+        elif operation == 'std_notify':
+            pass
+        
+        self.log.debug("<<< WorkflowStandardOperations - get_operation_control")
+
+        return None, ''
+        
+    def perform_operation(self, req, action, operation, old_state, new_state, res_wf_state, resource):
+        self.log.debug("---> Performing operation %s while transitioning from %s to %s."
+            % (operation, old_state, new_state))
+
+        if operation == 'set_owner':
+            if self.config.getbool(resource.realm, 'set_owners'):
+                target_owners = self.config.getstring(resource.realm, 'restrict_to_permission')
+                new_owner = target_owners.strip()
+            else:
+                new_owner = req.args.get('action_%s_operation_%s' % (action, operation), None)
+
+            if new_owner is not None and len(new_owner.strip()) > 0:
+                res_wf_state['owner'] = newowner.strip()
+            else:
+                self.log.debug("Unable to get the new owner!") 
+
+        elif operation == 'set_owner_to_self':
+            res_wf_state['owner'] = req.authname.strip()
+

tracgenericworkflow/trunk/tracgenericworkflow/templates/empty.html

File contents unchanged.