Index: timingandestimationplugin/api.py
===================================================================
--- timingandestimationplugin/api.py	(revision 2378)
+++ timingandestimationplugin/api.py	(working copy)
@@ -3,7 +3,6 @@
 import time
 from ticket_daemon import *
 from usermanual import *
-from reports import all_reports
 from trac.log import logger_factory
 from trac.ticket import ITicketChangeListener, Ticket
 from trac.core import *
@@ -11,6 +10,7 @@
 from trac.perm import IPermissionRequestor, PermissionSystem
 from webui import * 
 from ticket_webui import *
+from reportmanager import CustomReportManager
 
 ## report columns
 ## id|author|title|query|description
@@ -37,30 +37,54 @@
         
         """
     implements(IEnvironmentSetupParticipant)
+
+    db_version_key = None
+    db_version = None
+    db_installed_version = None
     
     """Extension point interface for components that need to participate in the
     creation and upgrading of Trac environments, for example to create
     additional database tables."""
     def __init__(self):
-        sql = "SELECT id, title FROM report ORDER BY ID"
+        # Setup logging
         dbhelper.mylog = self.log
-        self.reportmap = dbhelper.get_all(self.env.get_db_cnx(), sql)[1]
-        
+        self.db_version_key = 'TimingAndEstimationPlugin_Db_Version'
+        self.db_version = 5
+        self.db_installed_version = None
+
+        # Initialise database schema version tracking.
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT value FROM system WHERE name=%s", (self.db_version_key,))
+        try:
+            self.db_installed_version = int(cursor.fetchone()[0])
+        except:
+            self.db_installed_version = 0
+            cursor.execute("INSERT INTO system (name,value) VALUES(%s,%s)",
+                           (self.db_version_key, self.db_installed_version))
+            db.commit()
+            db.close()
+            print "Done"
+
     def environment_created(self):
         """Called when a new Trac environment is created."""
         if self.environment_needs_upgrade(None):
             self.upgrade_environment(None)
             
-    def db_needs_upgrade(self):
-        bill_date = dbhelper.db_table_exists(self.env.get_db_cnx(), 'bill_date');
-        report_version = dbhelper.db_table_exists(self.env.get_db_cnx(), 'report_version');
-        val =  dbhelper.db_needs_upgrade(self.env.get_db_cnx())
-        return val or not bill_date or not report_version
+    def system_needs_upgrade(self):
+        return self.db_installed_version < self.db_version
         
-    def db_do_upgrade(self):
-        bill_date = dbhelper.db_table_exists(self.env.get_db_cnx(), 'bill_date');
-        report_version = dbhelper.db_table_exists(self.env.get_db_cnx(), 'report_version');
-        if not bill_date:
+    def do_db_upgrade(self):
+        # Legacy support hack (supports upgrades from 0.1.6 to 0.1.7)
+        if self.db_installed_version == 0:
+            bill_date = dbhelper.db_table_exists(self.env.get_db_cnx(), 'bill_date');
+            report_version = dbhelper.db_table_exists(self.env.get_db_cnx(), 'report_version');
+            if bill_date and report_version:
+                self.db_installed_version = 1
+        # End Legacy support hack
+        
+        
+        if self.db_installed_version < 1:
             print "Creating bill_date table"
             sql = """
             CREATE TABLE bill_date (
@@ -71,8 +95,6 @@
             """
             dbhelper.execute_non_query(self.env.get_db_cnx(), sql)
             
-            
-        if not report_version:
             print "Creating report_version table"
             sql = """
             CREATE TABLE report_version (
@@ -82,136 +104,52 @@
             );
             """
             dbhelper.execute_non_query(self.env.get_db_cnx(), sql)
-        dbhelper.migrate_up_to_version(self.env.get_db_cnx, dbhelper)
-        dbhelper.set_plugin_db_version(self.env.get_db_cnx)
-    
-    def reports_need_upgrade(self):
-        bit = False
-        report_version = dbhelper.db_table_exists(self.env.get_db_cnx(), 'report_version');
-        if not report_version:
-            return True
-        
-        #make versions hash
-        try:
-            _versions = dbhelper.get_result_set(self.env.get_db_cnx(),"""
-               SELECT report as id, version, r.title as title
-               FROM report_version
-               JOIN report r ON r.Id = report_version.report
-               WHERE tags LIKE '%T&E%' """)
-        except Exception:
-            return True;
-        versions = {}
 
-        if _versions.rows == None:
-            return True
+        if self.db_installed_version < 4:
+            print "Upgrading report_version table to v4"
+            sql ="""
+            ALTER TABLE report_version ADD COLUMN tags varchar(1024) null;
+            """
+            dbhelper.execute_non_query(self.env.get_db_cnx(), sql)
 
-        for (id, version, title) in _versions.rows:
-            versions[title] = (id, version)
-
+        if self.db_installed_version < 5:
+            # In this version we convert to using reportmanager.py
+            # The easiest migration path is to remove all the reports!!
+            # They will be added back in later but all custom reports will be lost (deleted)
+            print "Dropping report_version table"
+            sql = "DELETE FROM report " \
+                  "WHERE author=%s AND id IN (SELECT report FROM report_version)"
+            dbhelper.execute_non_query(self.env.get_db_cnx(), sql, 'Timing and Estimation Plugin')
             
-        for report_group in all_reports:
-            rlist = report_group["reports"]
-            for report in rlist:
-                title = report["title"]
-                new_version = report["version"]
-                #the report map is a list of (id, name) tuples for the reports
-                # here we want to see if our report is in the list
-                idx = [i for i in range(0, len(self.reportmap)) if self.reportmap[i][1] == title]
-                if not idx:
-                    self.log.warning("Report '%s' needs to be added" % title)
-                    bit = True;
-                else:
-                    # If we had a report make sure its at the correct version
-                    if versions.has_key(title):
-                        (id, ver) = versions[title]
-                    else:
-                        ver = 0
-                    if ver < new_version:
-                        bit = True
-        return bit
-                
+            sql = "DROP TABLE report_version"
+            dbhelper.execute_non_query(self.env.get_db_cnx(), sql)
+        
+        
+        # This statement block always goes at the end this method
+        sql = "UPDATE system SET value=%s WHERE name=%s"
+        dbhelper.execute_non_query(self.env.get_db_cnx(),
+                                   sql, self.db_version, self.db_version_key)
+        self.db_installed_version = self.db_version
+    
+
     def do_reports_upgrade(self):
         self.log.debug( "Beginning Reports Upgrade");
-        #make version hash
-        _versions = dbhelper.get_result_set(self.env.get_db_cnx(),
-                                           """
-                                           SELECT report as id, version, r.title as title
-                                           FROM report_version
-                                           JOIN report r ON r.Id = report_version.report
-                                           WHERE tags LIKE '%T&E%'
-                                           """)
-        versions = {}
-        if _versions and _versions.rows:
-            for (id, version, title) in _versions.rows:
-                versions[title] = (id, version)
-            
-        biggestId = dbhelper.get_scalar(self.env.get_db_cnx(),
-                                        "SELECT ID FROM report ORDER BY ID DESC LIMIT 1")
-        def insert_report_version(id, ver, tag):
-            sql = "DELETE FROM report_version WHERE report = %s;"
-            dbhelper.execute_non_query(self.env.get_db_cnx(), sql, id  )
-            sql = """
-            INSERT INTO report_version (report, version, tags)
-            VALUES (%s, %s, %s);"""
-            # print "about to insert report_version"
-            dbhelper.execute_non_query(self.env.get_db_cnx(), sql, id, ver, tag )
-            # print "inserted report_version"
-            
-        for report_group in all_reports:
+        mgr = CustomReportManager(self.env, self.log)
+        r = __import__("reports", globals(), locals(), ["all_reports"])
+
+        for report_group in r.all_reports:
             rlist = report_group["reports"]
             group_title = report_group["title"]
-            tag = "T&E %s" % group_title
             for report in rlist:
                 title = report["title"]
                 new_version = report["version"]
-                report_id = [rid
-                             for (rid, map_title) in self.reportmap
-                             if map_title == title]
-                if not report_id:
-                    bit = True; 
-                    sql = """INSERT INTO report (id,author,title,query,description) 
-                             VALUES (%s,'Timing and Estimation Plugin',%s,%s,'') """
-                    biggestId += 1
-                    dbhelper.execute_non_query(self.env.get_db_cnx(), sql,
-                                               biggestId, title, report["sql"])
-                    insert_report_version(biggestId, new_version, tag)
+                mgr.add_report(report["title"], "Timing and Estimation Plugin", \
+                               "", report["sql"], \
+                               report["uuid"], report["version"],
+                               "Timing and Estimation Plugin",
+                               group_title)
 
-                    report["reportnumber"] = biggestId
-                    self.reportmap.extend([(biggestId,title)])
-                else:
-                    report_id = report_id[0]
-                    report["reportnumber"] = report_id
-                     # If we had a report make sure its at the correct version
-                    if versions.has_key(title):
-                        ( _ , ver) = versions[title]
-                    else:
-                        ver = 0
-                    if not ver:
-                        sql = """
-                        UPDATE report
-                        SET query=%s 
-                        WHERE id = %s """
-                        print "updating report: %s" % title
-                        dbhelper.execute_non_query(self.env.get_db_cnx(), sql,
-                                                   report["sql"], report_id )
-                        insert_report_version( report_id, new_version, tag )
-                    elif ver < new_version:
-                        sql = """
-                        UPDATE report
-                        SET query=%s 
-                        WHERE id = %s """
-                        print "updating report to new version: %s" % title
-                        dbhelper.execute_non_query(self.env.get_db_cnx(), sql,
-                                                   report["sql"], report_id )
-                        
-                        sql = """
-                        UPDATE report_version
-                        SET version = %s, tags = %s
-                        
-                        WHERE report = %s
-                        """
-                        dbhelper.execute_non_query(self.env.get_db_cnx(), sql, new_version, tag, report_id)
-                        
+    
     def ticket_fields_need_upgrade(self):
         ticket_custom = "ticket-custom"
         return not ( self.config.get( ticket_custom, "totalhours" ) and \
@@ -283,9 +221,8 @@
         performed, `False` otherwise.
 
         """
-        return (self.db_needs_upgrade()) or \
+        return (self.system_needs_upgrade()) or \
                (self.ticket_fields_need_upgrade()) or \
-               (self.reports_need_upgrade()) or \
                (self.needs_user_man()) 
             
     def upgrade_environment(self, db):
@@ -299,15 +236,14 @@
             print s
             return True
         print "Timing and Estimation needs an upgrade"
-        if self.db_needs_upgrade():
-            p("Upgrading Database")
-            self.db_do_upgrade()
+        p("Upgrading Database")
+        self.do_db_upgrade()
+        p("Upgrading reports")
+        self.do_reports_upgrade()
+
         if self.ticket_fields_need_upgrade():
             p("Upgrading fields")
             self.do_ticket_field_upgrade()
-        if self.reports_need_upgrade():
-            p("Upgrading reports")
-            self.do_reports_upgrade()
         if self.needs_user_man():
             p("Upgrading usermanual")
             self.do_user_man_update()
Index: timingandestimationplugin/webui.py
===================================================================
--- timingandestimationplugin/webui.py	(revision 2378)
+++ timingandestimationplugin/webui.py	(working copy)
@@ -3,7 +3,6 @@
 import datetime
 import dbhelper
 from usermanual import * 
-from reports import all_reports
 from trac.log import logger_factory
 from trac.core import *
 from trac.web import IRequestHandler
@@ -11,39 +10,13 @@
 from trac.web.chrome import add_stylesheet, add_script, \
      INavigationContributor, ITemplateProvider
 from trac.web.href import Href
+from reportmanager import CustomReportManager
 
 
 
 class TimingEstimationAndBillingPage(Component):
     implements(INavigationContributor, IRequestHandler, ITemplateProvider)
 
-    #CACHING THIS WAS CAUSING PROBLEMS ON A GLOBAL INSTALL
-    def get_copy_report_hash_for_render(self, req):
-        new_reports = []
-        sql = "SELECT id, title FROM report ORDER BY ID"
-        reportmap = dbhelper.get_all(self.env.get_db_cnx(), sql)[1]
-        
-        for report_group in all_reports:
-            new_group = { "title" : report_group["title"] }
-            sql = """
-            SELECT id, title
-            FROM report
-            JOIN report_version on report.id = report_version.report
-            WHERE tags LIKE '%%%s%%'
-            ORDER BY ID""" % report_group["title"]
-            reportmap = dbhelper.get_all(self.env.get_db_cnx(), sql)[1]
-            new_reports_list = []
-            
-            for (r_id, r_name) in reportmap:
-                #find the report id for the name
-                new_report = {"title" : r_name,
-                              "reportnumber" : r_id,
-                              "href" : "%s/%s" % (req.href.report(), r_id)}
-                new_reports_list.extend([ new_report ])
-            new_group["reports"] = new_reports_list
-            new_reports.extend([new_group])
-        return new_reports
-            
     def __init__(self):
         pass
 
@@ -111,12 +84,14 @@
             if req.args.has_key('setbillingtime'):
                 self.set_bill_date(req.authname)
                 addMessage("All tickets last bill date updated")
-                
-        req.hdf["billing_info"] = {"messages": messages,
-                                   "href":req.href.Billing(),
-                                   "reports": self.get_copy_report_hash_for_render(req),
-                                   "usermanual_href":req.href.wiki(user_manual_wiki_title),
-                                   "usermanual_title":user_manual_title
+
+        mgr = CustomReportManager(self.env, self.log)
+        req.hdf["billing_info"] = {"messages":         messages,
+                                   "href":             req.href.Billing(),
+                                   "report_base_href": req.href.report(),
+                                   "reports":          mgr.get_reports_by_group("Timing and Estimation Plugin"),
+                                   "usermanual_href":  req.href.wiki(user_manual_wiki_title),
+                                   "usermanual_title": user_manual_title
                                    }
         self.set_request_billing_dates(req)
         add_stylesheet(req, "Billing/billingplugin.css")
Index: timingandestimationplugin/usermanual.py
===================================================================
--- timingandestimationplugin/usermanual.py	(revision 2378)
+++ timingandestimationplugin/usermanual.py	(working copy)
@@ -1,8 +1,8 @@
 user_manual_title = "Timing and Estimation Plugin User Manual"
-user_manual_version = 9
+user_manual_version = 10
 user_manual_wiki_title = "TimingAndEstimationPluginUserManual"
 user_manual_content = """
-[[TOC]]
+[[PageOutline]]
 = Timing and Estimation Plugin User Manual =
 [http://trac-hacks.org/wiki/TimingAndEstimationPlugin TimingAndEstimationPlugin on TracHacks] | [http://trac-hacks.org/report/9?COMPONENT=TimingAndEstimationPlugin Open Tickets] | [http://trac-hacks.org/newticket?component=TimingAndEstimationPlugin&owner=bobbysmith007 New Ticket]  | 
 [http://trac-hacks.org/browser/timingandestimationplugin/trunk Web Browsable Source Code]
@@ -52,16 +52,19 @@
 === Adding More Reports ===
 To add reports to the Management screen sections, you must run the following sql against your trac database
 Remember to fill in the @reportID of the report you want to insert, and to select the insert statement for the section of your choice.
- * '''INSERT INTO report_version (report, version, tags) VALUES ( @reportID , 1, 'Ticket/Hour Reports');'''
- * '''INSERT INTO report_version (report, version, tags) VALUES ( @reportID , 1, 'Billing Reports');'''
+ * {{{INSERT INTO custom_report (id, uuid, maingroup, subgroup, version, ordering) VALUES (@reportID , @uuid, 'Timing and Estimation Plugin', 'Billing Reports', 1, 0);}}}
+ * {{{INSERT INTO custom_report (id, uuid, maingroup, subgroup, version, ordering) VALUES (@reportID , @uuid, 'Timing and Estimation Plugin', 'Ticket/Hour Reports', 1, 0);}}}
 
+''NB: @uuid is a globally uninque identifier created via a tool such as {{{uuidgen}}} on Linux or various [http://www.famkruithof.net/uuid/uuidgen online tools]. It is used in this plugin to provide programatic reference to specific reports such that they can be upgraded successfully on future revisions of the plugin''
+
 === Removing a Report ===
 To remove reports from the Management page, run the following query. 
-Remember to fill in the @reportID of the report you want to insert, and to select the insert statement for the section of your choice.
+Remember to fill in the @reportID of the report you want to modify.
  * To remove for this version of the plugin (will be over written in future plugin upgrades)
-   * '''UPDATE report_version SET tags=!'' WHERE report = @reportID ;'''
+   * {{{UPDATE custom_report SET maingroup='x'||maingroup WHERE report = @reportID;}}}
  * To remove permanently (wont be over written in future plugin upgrades)
-   * '''UPDATE report_version SET version=9999, tags=!'' WHERE report = @reportID ;'''
+   * {{{UPDATE custom_report SET version=9999, maingroup='x'||maingroup WHERE report = @reportID;}}}
+''NB: The 'x' part is not important - you just need to make the column read something other than 'Timing and Estimation Plugin'.''
 
 === TAKE NOTE ===
  '''The reports can only be called from the Management Page. They will not work from the Trac View Tickets page. (Due to the custom variables that need values).'''
Index: timingandestimationplugin/reports.py
===================================================================
--- timingandestimationplugin/reports.py	(revision 2378)
+++ timingandestimationplugin/reports.py	(working copy)
@@ -6,6 +6,7 @@
 
 billing_reports = [
         {
+    "uuid":"b24f08c0-d41f-4c63-93a5-25e18a8513c2",
     "title":"Ticket Work Summary",
     "reportnumber":None,
     "version":14,
@@ -47,6 +48,7 @@
     """
     },#END Ticket work summary
         {
+    "uuid":"af13564f-0e36-4a17-96c0-632dc68d8d14",
     "title":"Milestone Work Summary",
     "reportnumber":None,
     "version":13,
@@ -94,6 +96,7 @@
     },#END Milestone work summary
         
     {
+    "uuid":"7bd4b0ce-da6d-4b11-8be3-07e65b540d99",
     "title":"Developer Work Summary",
     "reportnumber":None,
     "version":13,
@@ -135,6 +138,7 @@
 ]
 ticket_hours_reports = [
 {
+    "uuid":"8d785cdb-dcf5-43c9-b2a6-216997b0011a",
     "title": "Ticket Hours",
     "reportnumber": None,
     "version":10,
@@ -204,6 +208,7 @@
     },
 #END Ticket Hours 
 {
+    "uuid":"71e7c36d-e512-4d0b-b499-087d4d20ff0b",
     "title": "Ticket Hours with Description",
     "reportnumber": None,
     "version":11,
@@ -282,6 +287,7 @@
 #END Ticket Hours 
 
     {
+    "uuid":"5f33b102-e6a6-47e8-976c-ac7a6794a909",
     "title":"Ticket Hours Grouped By Component",
     "reportnumber":None,
     "version":10,
@@ -362,6 +368,7 @@
 # END Ticket Hours  GROUPED BY COMPONENT
     
     {
+    "uuid":"7816f034-a174-4a94-aed6-358fb648b2fc",
     "title":"Ticket Hours Grouped By Component with Description",
     "reportnumber":None,
     "version":9,
@@ -437,6 +444,7 @@
     },
 # END Ticket Hours Grouped BY Component with Description
     {
+    "uuid":"03815803-7688-4f3a-8e65-8d254cc1d1fb",
     "title":"Ticket Hours Grouped By Milestone",
     "reportnumber":None,
     "version":10,
@@ -513,6 +521,7 @@
     },
 #END Ticket Hours Grouped By MileStone
         {
+    "uuid":"040c9025-7641-4d18-96ad-2b26b4095566",
     "title":"Ticket Hours Grouped By MileStone with Description",
     "reportnumber":None,
     "version":10,
Index: timingandestimationplugin/migrate/upgrade4.py
===================================================================
--- timingandestimationplugin/migrate/upgrade4.py	(revision 2378)
+++ timingandestimationplugin/migrate/upgrade4.py	(working copy)
@@ -1,10 +0,0 @@
-
-
-def up ( db_fn, helper ):
-    print "Running migrate version 4"
-    sql ="""
-    ALTER TABLE report_version ADD COLUMN tags varchar(1024) null;
-    """
-    helper.execute_non_query(db_fn(), sql)
-
-
Index: timingandestimationplugin/dbhelper.py
===================================================================
--- timingandestimationplugin/dbhelper.py	(revision 2378)
+++ timingandestimationplugin/dbhelper.py	(working copy)
@@ -1,5 +1,3 @@
-db_version_key = 'TimingAndEstimationPlugin_Db_Version';
-db_version = 4
 mylog = None;
 
 
@@ -60,24 +58,6 @@
     else:
         return None;
 
-def get_plugin_db_version(db):
-    sql = "SELECT value FROM system where name = '%s';" % db_version_key
-    val = get_scalar(db, sql)
-    return val;
-
-def set_plugin_db_version(db_fn):
-    if get_plugin_db_version(db_fn()):
-        sql = "UPDATE system SET value='%s' WHERE name = '%s'" % (db_version, db_version_key)
-    else:
-        sql = "INSERT INTO system (name, value) VALUES( '%s', '%s') " % ( db_version_key, str(db_version) )
-    execute_non_query(db_fn(), sql);
-
-def db_needs_upgrade(db):
-    ver = get_plugin_db_version(db);
-    if not ver or int(ver) < int(db_version):
-        return True
-    return False
-
 def db_table_exists(db, table):
     sql = "SELECT * FROM %s LIMIT 1" % table;
     cur = db.cursor()
@@ -103,25 +83,7 @@
     """Executes the query and returns a Result Set"""
     return ResultSet(get_all(db, sql, *params))
 
-def migrate_up_to_version( db_fn, helper ):
-    try:
-        start = int(get_plugin_db_version(db_fn()))
-    except Exception:
-        start = 0
-    end = db_version
-    r = range( start+1, end+1)
-    import migrate
-    for i in  r:
-        key = "upgrade"+str(i)
-        try:
-            m = __import__("migrate."+key, globals(), locals(), ["up"])
-            m.up(db_fn, helper)
-        except Exception, e:
-            #we were not using this sytem before version 4
-            if i >= 4:
-                print "no migration for %s " % key
 
-
 class ResultSet:
     """ the result of calling getResultSet """
     def __init__ (self, (columnDescription, rows)):
Index: timingandestimationplugin/reportmanager.py
===================================================================
--- timingandestimationplugin/reportmanager.py	(revision 0)
+++ timingandestimationplugin/reportmanager.py	(revision 0)
@@ -0,0 +1,142 @@
+from trac.core import *
+
+
+class CustomReportManager:
+  """A Class to manage custom reports"""
+  version = 1
+  name = "custom_report_manager_version"
+  env = None
+  log = None
+  
+  def __init__(self, env, log):
+    self.env = env
+    self.log = log
+    self.upgrade()
+  
+  def upgrade(self):  
+    # Check to see what version we have
+    db = self.env.get_db_cnx()
+    cursor = db.cursor()
+    cursor.execute("SELECT value FROM system WHERE name=%s", (self.name,))
+    try:
+      version = int(cursor.fetchone()[0])
+    except:
+      version = 0
+      cursor.execute("INSERT INTO system (name,value) VALUES(%s,%s)",
+                     (self.name, version))
+        
+    if version > self.version:
+      raise TracError("Fatal Error: You appear to be running two plugins with conflicting versions "
+                      "of the CustomReportManager class. Please ensure that '%s' is updated to "
+                      "version %s of the file reportmanager.py (currently using version %s)."
+                      % (__name__, str(version), str(self.version)))
+    
+    # Do the staged updates
+    try:
+      if version < 1:
+        cursor.execute("CREATE TABLE custom_report ("
+                       "id         INTEGER,"
+                       "uuid       VARCHAR(64),"
+                       "maingroup  VARCHAR(255),"
+                       "subgroup   VARCHAR(255),"
+                       "version    INTEGER,"
+                       "ordering   INTEGER)")
+      
+      #if version < 2:
+      #  cursor.execute("...")
+    
+      # Updates complete, set the version
+      cursor.execute("UPDATE system SET value=%s WHERE name=%s", 
+                     (self.version, self.name))
+      db.commit()
+      db.close()
+    
+    except Exception, e:
+      self.log.error("CustomReportManager Exception: %s" % (e,));
+      db.rollback()
+  
+  def add_report(self, title, author, description, query, uuid, version, maingroup, subgroup=""):
+    # First check to see if we can load an existing version of this report
+    db = self.env.get_db_cnx()
+    cursor = db.cursor()
+    try:
+      cursor.execute("SELECT id, version FROM custom_report "
+                     "WHERE uuid=%s", (uuid,))
+      (id, currentversion) = cursor.fetchone()          
+    except:
+      id = None
+      currentversion = 0
+    
+    try:
+      if not id:
+        cursor.execute("SELECT MAX(id) FROM report")
+        next_id = int(cursor.fetchone()[0]) + 1
+        self.log.debug("Inserting new report with uuid '%s'" % (uuid,));
+
+        # Get the ordering of any current reports in this group/subgroup.
+        try:
+          cursor.execute("SELECT MAX(ordering) FROM custom_report "
+                         "WHERE maingroup=%s AND subgroup=%s", (maingroup, subgroup))
+          ordering = int(cursor.fetchone()[0]) + 1
+        except:
+          ordering = 0
+        
+        cursor.execute("INSERT INTO report (id, title, author, description, query) "
+                       "VALUES (%s, %s, %s, %s, %s)",
+                       (next_id, title, author, description, query))
+        cursor.execute("INSERT INTO custom_report (id, uuid, maingroup, subgroup, version, ordering) "
+                       "VALUES (%s, %s, %s, %s, %s, %s)",
+                       (next_id, uuid, maingroup, subgroup, version, ordering))
+        db.commit()
+        db.close()
+        return True
+      if currentversion < version:
+        self.log.debug("Updating report with uuid '%s' to version %s" % (uuid,version));
+        cursor.execute("UPDATE report SET title=%s, author=%s, description=%s, query=%s "
+                       "WHERE id=%s", (title, author, description, query, id))
+        cursor.execute("UPDATE custom_report SET version=%s, maingroup=%s, subgroup=%s "
+                       "WHERE id=%s", (version, maingroup, subgroup, id))
+        db.commit()
+        db.close()
+        return True
+    except Exception, e:
+      self.log.error("CustomReportManager Exception: %s" % (e,));
+      db.rollback()
+    
+    return False
+  
+  def get_report_by_uuid(self, uuid):
+    db = self.env.get_db_cnx()
+    cursor = db.cursor()
+    rv = None
+    try:
+      cursor.execute("SELECT report.id,report.title FROM custom_report "
+                     "LEFT JOIN report ON custom_report.id=report.id "
+                     "WHERE custom_report.uuid=%s", (uuid,))
+      row = cursor.fetchone()
+      rv = (row[0], row[1])
+    except:
+      pass
+    
+    return rv
+  
+  def get_reports_by_group(self, group):
+    db = self.env.get_db_cnx()
+    cursor = db.cursor()
+    rv = {}
+    try:
+      cursor.execute("SELECT custom_report.subgroup,report.id,report.title "
+                     "FROM custom_report "
+                     "LEFT JOIN report ON custom_report.id=report.id "
+                     "WHERE custom_report.maingroup=%s "
+                     "ORDER BY custom_report.subgroup,custom_report.ordering", (group,))
+      for subgroup,id,title in cursor:
+        if not rv.has_key(subgroup):
+          rv[subgroup] = { "title": subgroup,
+                           "reports": [] }
+        rv[subgroup]["reports"].append( { "id": int(id), "title": title } )
+    except:
+      pass
+    
+    return rv
+

Property changes on: timingandestimationplugin/reportmanager.py
___________________________________________________________________
Name: svn:executable
   + *
Name: svn:mime-type
   + text/x-python
Name: svn:eol-style
   + native

Index: timingandestimationplugin/templates/billing.cs
===================================================================
--- timingandestimationplugin/templates/billing.cs	(revision 2378)
+++ timingandestimationplugin/templates/billing.cs	(working copy)
@@ -69,7 +69,7 @@
     <li><?cs var:report_group.title ?>
       <ul>
         <?cs each:report = report_group.reports ?>
-          <li><a href="" onmouseover="linkify(this, '<?cs var:report.href ?>')"  >
+          <li><a href="" onmouseover="linkify(this, '<?cs var:billing_info.report_base_href ?>/<?cs var:report.id ?>')"  >
             <?cs var:report.title ?>
           </a></li>     
        <?cs /each ?>
@@ -81,4 +81,4 @@
 
 </div>
 </form>
-<?cs include "footer.cs"?>
\ No newline at end of file
+<?cs include "footer.cs"?>
Index: setup.py
===================================================================
--- setup.py	(revision 2378)
+++ setup.py	(working copy)
@@ -7,8 +7,8 @@
 setup(name=PACKAGE,
       description='Plugin to make Trac support time estimation and tracking',
       keywords='trac plugin estimation timetracking',
-      version='0.4.4',
-      url='',
+      version='0.4.5',
+      url='http://www.trac-hacks.org/wiki/TimingAndEstimationPlugin',
       license='http://www.opensource.org/licenses/mit-license.php',
       author='Russ Tyndall at Acceleration.net',
       author_email='russ@acceleration.net',
@@ -18,7 +18,7 @@
       See http://trac-hacks.org/wiki/TimingAndEstimationPlugin for details.
       """,
       packages=[PACKAGE],
-      package_data={PACKAGE : ['templates/*.cs', 'htdocs/*', 'migrate/*.py']},
+      package_data={PACKAGE : ['templates/*.cs', 'htdocs/*']},
       entry_points={'trac.plugins': '%s = %s' % (PACKAGE, PACKAGE)})
 
 
@@ -43,3 +43,8 @@
 ## jonas
 ## made it so that base_url was unnecessary
 
+## Colin Guthrie
+## trac-hacks user: coling
+## Refactored the custom reports code to make it
+## easy for other plugins to provide reports to
+## compliment those provided by default
