Commits

takanao ENDOH committed 9569014

Comments (0)

Files changed (10)

+Metadata-Version: 1.0
+Name: TracGantt
+Version: 0.3.2a
+Summary: This is a Gantt-Chart creation plugin for Trac.
+Home-page: http://willbarton.com/code/tracgantt/
+Author: Will Barton
+Author-email: wbb4@opendarwin.org
+License: UNKNOWN
+Description: UNKNOWN
+Platform: UNKNOWN
+TracGantt 0.3
+Released July 12, 2006
+Will Barton <wbb4@opendarwin.org>
+http://ideas.water-powered.com/gantt
+
+The TracGantt plugin is very easy to use, and only requires a few very 
+simple steps to set up. First, you will need to download the plugin, 
+either in Python egg form, or in source form.
+
+To install from a Python egg, download the egg, and then place it in 
+your Trac instance's plugins directory and restart tracd, if that is how
+you are running Trac. Additional Trac plugin documentation can be found 
+in the Trac documentation.
+
+To install from source, download the source file and unarchive it, then
+run:
+
+	$ python setup.py bdist_egg
+
+To build a Python egg. Once the egg file is built, copy it to the Trac 
+plugins directory as described above.
+
+Next, you will need to edit your Trac config file to add custom fields 
+to tickets, as well as set the expected date format of those fields.
+
+The first things to add are the new ticket fields. If you already have 
+a ticket-custom section, append these to it:
+
+	[ticket-custom]
+	due_assign = text
+	due_assign.label = Due to assign
+	due_assign.value = DD/MM/YYYY
+
+	dependencies = text
+	dependencies.label = Dependencies
+	dependencies.value =
+
+	due_close= text
+	due_close.label = Due to close
+	due_close.value = DD/MM/YYYY
+
+	include_gantt = checkbox
+	include_gantt.label = Include in GanttChart
+	include_gantt.value =
+
+This will add four new fields to tickets, a "Due to assign" field, which 
+contains the date by which this ticket should be assigned, a 
+"Dependencies" field, for listing ticket numbers upon which this ticket 
+depends, a "Due to close" field, which contains the date by which this 
+ticket should be closed, and finally a checkbox that allows the ticket 
+to be included in Gantt charts.
+
+In addition, TracGantt provides several tweakable configuration knobs
+that you can use to change the behavior of the gantt charts. They are
+listed below with their default values.
+
+	[gantt-charts]
+	# The format of dates entered by humans in the above ticket fields
+	date_format = %m/%d/%Y
+
+	# Include the ticket summary in the gantt chart display
+	include_summary = true
+
+	# Trim the included summary to the given number of characters
+	summary_length = 16
+
+	# Use the creation date of a ticket as the "due assign" date if no
+	# assignment date is given
+	use_creation_date = true
+
+	# Show on the gantt chart the date the ticket was opened, to contrast
+	# with the assignment date.
+	show_opened = true
+
+NOTE If you are placing the module anywhere outside of Trac's standard
+'plugins' directory (i.e. to share across Trac instances), then you will
+also need to add:
+
+	[components]
+	tracgantt.* = enabled
+
+to your Trac config file. Again, this is only necessary if the egg file
+is placed outside of the Trac 'plugins' folder, in a standard Python
+search path.
+
+If you are using Trac with Apache and mod_python, you may also need to
+restart Apache, to avoid the plugin being accessible from one Apache
+process, but not others.

TracGantt.egg-info/top_level.txt

+tracgantt

TracGantt.egg-info/trac_plugin.txt

+tracgantt
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+    from ez_setup import use_setuptools
+    use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6b4"
+DEFAULT_URL     = "http://cheeseshop.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+    'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+    'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+    'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+    'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+    'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+    'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+    'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+    'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+}
+
+import sys, os
+
+def _validate_md5(egg_name, data):
+    if egg_name in md5_data:
+        from md5 import md5
+        digest = md5(data).hexdigest()
+        if digest != md5_data[egg_name]:
+            print >>sys.stderr, (
+                "md5 validation of %s failed!  (Possible download problem?)"
+                % egg_name
+            )
+            sys.exit(2)
+    return data
+
+
+def use_setuptools(
+    version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+    download_delay=15
+):
+    """Automatically find/download setuptools and make it available on sys.path
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end with
+    a '/').  `to_dir` is the directory where setuptools will be downloaded, if
+    it is not already available.  If `download_delay` is specified, it should
+    be the number of seconds that will be paused before initiating a download,
+    should one be required.  If an older version of setuptools is installed,
+    this routine will print a message to ``sys.stderr`` and raise SystemExit in
+    an attempt to abort the calling script.
+    """
+    try:
+        import setuptools
+        if setuptools.__version__ == '0.0.1':
+            print >>sys.stderr, (
+            "You have an obsolete version of setuptools installed.  Please\n"
+            "remove it from your system entirely before rerunning this script."
+            )
+            sys.exit(2)
+    except ImportError:
+        egg = download_setuptools(version, download_base, to_dir, download_delay)
+        sys.path.insert(0, egg)
+        import setuptools; setuptools.bootstrap_install_from = egg
+
+    import pkg_resources
+    try:
+        pkg_resources.require("setuptools>="+version)
+
+    except pkg_resources.VersionConflict:
+        # XXX could we install in a subprocess here?
+        print >>sys.stderr, (
+            "The required version of setuptools (>=%s) is not available, and\n"
+            "can't be installed while this script is running. Please install\n"
+            " a more recent version first."
+        ) % version
+        sys.exit(2)
+
+def download_setuptools(
+    version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+    delay = 15
+):
+    """Download setuptools from a specified location and return its filename
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end
+    with a '/'). `to_dir` is the directory where the egg will be downloaded.
+    `delay` is the number of seconds to pause before an actual download attempt.
+    """
+    import urllib2, shutil
+    egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+    url = download_base + egg_name
+    saveto = os.path.join(to_dir, egg_name)
+    src = dst = None
+    if not os.path.exists(saveto):  # Avoid repeated downloads
+        try:
+            from distutils import log
+            if delay:
+                log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help).  I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+   %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+                    version, download_base, delay, url
+                ); from time import sleep; sleep(delay)
+            log.warn("Downloading %s", url)
+            src = urllib2.urlopen(url)
+            # Read/write all in one block, so we don't create a corrupt file
+            # if the download is interrupted.
+            data = _validate_md5(egg_name, src.read())
+            dst = open(saveto,"wb"); dst.write(data)
+        finally:
+            if src: src.close()
+            if dst: dst.close()
+    return os.path.realpath(saveto)
+
+def main(argv, version=DEFAULT_VERSION):
+    """Install or upgrade setuptools and EasyInstall"""
+
+    try:
+        import setuptools
+    except ImportError:
+        import tempfile, shutil
+        tmpdir = tempfile.mkdtemp(prefix="easy_install-")
+        try:
+            egg = download_setuptools(version, to_dir=tmpdir, delay=0)
+            sys.path.insert(0,egg)
+            from setuptools.command.easy_install import main
+            return main(list(argv)+[egg])   # we're done here
+        finally:
+            shutil.rmtree(tmpdir)
+    else:
+        if setuptools.__version__ == '0.0.1':
+            # tell the user to uninstall obsolete version
+            use_setuptools(version)
+
+    req = "setuptools>="+version
+    import pkg_resources
+    try:
+        pkg_resources.require(req)
+    except pkg_resources.VersionConflict:
+        try:
+            from setuptools.command.easy_install import main
+        except ImportError:
+            from easy_install import main
+        main(list(argv)+[download_setuptools(delay=0)])
+        sys.exit(0) # try to force an exit
+    else:
+        if argv:
+            from setuptools.command.easy_install import main
+            main(argv)
+        else:
+            print "Setuptools version",version,"or greater has been installed."
+            print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+
+
+def update_md5(filenames):
+    """Update our built-in md5 registry"""
+
+    import re
+    from md5 import md5
+
+    for name in filenames:
+        base = os.path.basename(name)
+        f = open(name,'rb')
+        md5_data[base] = md5(f.read()).hexdigest()
+        f.close()
+
+    data = ["    %r: %r,\n" % it for it in md5_data.items()]
+    data.sort()
+    repl = "".join(data)
+
+    import inspect
+    srcfile = inspect.getsourcefile(sys.modules[__name__])
+    f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+    match = re.search("\nmd5_data = {\n([^}]+)}", src)
+    if not match:
+        print >>sys.stderr, "Internal error!"
+        sys.exit(2)
+
+    src = src[:match.start(1)] + repl + src[match.end(1):]
+    f = open(srcfile,'w')
+    f.write(src)
+    f.close()
+
+
+if __name__=='__main__':
+    if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+        update_md5(sys.argv[2:])
+    else:
+        main(sys.argv[1:])
+
+
+
+
+
+from ez_setup import use_setuptools
+use_setuptools()
+
+from setuptools import setup, find_packages
+
+setup (
+        name = "TracGantt",
+        version = "0.3.2a",
+        packages = ['tracgantt'],
+        package_data={'tracgantt': ['templates/*.cs', 'htdocs/*.css']},
+
+        #install_requires = ['trac>=0.9'],
+        #entry_points = {'trac.plugins': ['module_name = gantt']},
+        
+        author = "Will Barton",
+        author_email = "wbb4@opendarwin.org",
+        description = "This is a Gantt-Chart creation plugin for Trac.",
+        url = "http://willbarton.com/code/tracgantt/",
+        )

tracgantt/__init__.py

+from gantt import *

tracgantt/gantt.py

+# Copyright (c) 2005, 2006 Will Barton
+# All rights reserved.
+#
+# Author: Will Barton <wbb4@opendarwin.org>
+# 
+# Redistribution and use in source and binary forms, with or without 
+# modification, are permitted provided that the following conditions
+# are met:
+# 
+#   1. Redistributions of source code must retain the above copyright 
+#      notice, this list of conditions and the following disclaimer.
+#   2. 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.
+#   3. The name of the author may not be used to endorse or promote products
+#      derived from this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED ``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 AUTHOR 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.
+
+# Standard Python modules
+import re, datetime, time
+import tempfile, os
+import sys, traceback
+
+
+# Trac
+from trac import util
+from trac.core import *
+#from trac.core import ComponentManager
+from trac.web import IRequestHandler
+from trac.web.chrome import add_link, add_stylesheet, INavigationContributor, ITemplateProvider
+from trac.perm import IPermissionRequestor, PermissionSystem
+from trac.ticket.model import Ticket
+from trac import db_default
+
+class GanttComponent(Component):
+
+    implements(INavigationContributor, IRequestHandler,
+            ITemplateProvider, IPermissionRequestor)
+
+    # INavigationContributor
+    def get_active_navigation_item(self, req):
+        return 'gantt'
+    def get_navigation_items(self, req):
+        yield ('mainnav', 'gantt', 
+            util.Markup('<a href="%s">Gantt Charts</a>' \
+                                       % self.env.href.gantt()))
+
+    # ITemplateProvider
+    def get_htdocs_dirs(self):
+        from pkg_resources import resource_filename
+        return [('gantt',resource_filename(__name__, 'htdocs'))]
+    def get_templates_dirs(self):
+        from pkg_resources import resource_filename
+        return [resource_filename(__name__, 'templates')]
+
+
+    # IPermissionRequestor methods
+    def get_permission_actions(self):
+        actions = ['GANTT_VIEW']
+        return actions + [('GANTT_ADMIN', actions)]
+
+    # IRequestHandler methods
+    def match_request(self, req):
+        match = re.match(r'/gantt(?:/([0-9]+))?', req.path_info)
+        if match:
+            if match.group(1):
+                req.args['id'] = match.group(1)
+            return 1
+
+    def process_request(self, req):
+        # We require both the ability to view reports and the ability to
+        # view gantt charts, since gantt charts are from reports.
+        req.perm.assert_permission('REPORT_VIEW')
+        req.perm.assert_permission('GANTT_VIEW')
+
+        id = int(req.args.get('id', -1))
+        action = req.args.get('action', 'list')
+        
+        db = self.env.get_db_cnx()
+
+        if id == -1:
+            title = 'Available Charts'
+            description = 'This is a list of charts available.'
+            
+            cols,rows = self._reports(db)
+            
+            req.hdf['gantt.id'] = -1
+            req.hdf['title'] = title
+            req.hdf['description'] = description
+            req.hdf['cols'] = cols
+            req.hdf['rows'] = rows
+
+            add_stylesheet(req, 'common/css/report.css')
+        else:
+            add_link(req, 'up', self.env.href.gantt(), 'Available Charts')
+            report = self._report_for_id(db, id)
+
+            if report['id'] > 0: 
+                report['title'] = '{%i} %s' % (report['id'], report['title'])
+
+            req.hdf['title'] = report['title']
+            req.hdf['gantt.title'] = report['title']
+            req.hdf['gantt.id'] = report['id']
+
+            show_opened = self.env.config.getbool('gantt-charts',
+                    'show_opened', 'false')
+
+            tickets,dates,broken = self._tickets_for_report(db, report['query'])
+            tickets,dates = self._paginate_tickets(tickets, dates)
+
+            req.hdf['gantt.tickets'] = tickets
+            req.hdf['gantt.dates'] = dates
+            req.hdf['gantt.broken'] = broken
+            req.hdf['gantt.broken_no'] = len(broken)
+            req.hdf['gantt.show_opened'] = show_opened
+
+
+        add_stylesheet(req, 'gantt/gantt.css')
+        return 'gantt.cs', None
+
+    def _reports(self, db):
+        cursor = db.cursor()
+        cursor.execute("SELECT id AS report,title FROM report ORDER BY "
+                + "report")
+        info = cursor.fetchall() or []
+        cols = [s[0] for s in cursor.description or []]
+        db.rollback()
+        rows = [{'report':i[0], 'title':i[1], 
+                'href':self.env.href.gantt(i[0])} for i in info]
+        return cols, rows
+
+    def _report_for_id(self, db, id):
+        cursor = db.cursor()
+
+        # The 'sql' column changed to 'query' in 0.10, so we want to
+        # continue supporting both cases.
+        # This happened at http://trac.edgewall.org/changeset/3300,
+        # apparently the new db_version is 19 (with the query column)
+        if db_default.db_version >= 19:
+            cursor.execute("SELECT title,query,description from report " \
+                    + "WHERE id=%s", (id,))
+        else:
+            cursor.execute("SELECT title,sql,description from report " \
+                    + "WHERE id=%s", (id,))
+ 
+        row = cursor.fetchone()
+        if not row:
+            raise util.TracError('Report %d does not exist.' % id, 
+                'Invalid Report Number')
+        title = row[0] or ''
+        query = row[1]
+        description = row[2] or ''
+
+        return {'id':id, 'title':title, 'query':query, 'description':description}
+
+    def _tickets_for_report(self, db, query):
+        """ Get a list of Ticket instances for the tickets in a report """
+
+        tickets = []
+        dates = []
+        broken = []
+
+        ## Get tickets for this report
+        cursor = db.cursor()
+        cursor.execute(query)
+        info = cursor.fetchall() or []
+        cols = [s[0] for s in cursor.description or []]
+        db.rollback()
+
+        ## Functions for processing the SQL results into the datatypes
+        ## we need
+
+        # Function to check if ticket is included in the gantt chart
+        ticket_in_gantt = lambda t : \
+                int(t.values.get('include_gantt', 0)) != 0
+
+        # Function to strip everything but numbers out of the given
+        # string, and create an int.  Closed ticket ids have a unicode
+        # checkmark.
+        # XXX: This is ugly, since we can't guarnatee types on ticket
+        # ids, we cast to a string, then replace any chars, and then
+        # back to int.
+        ticket_id = lambda i : int(re.sub("[^0-9]*", "", str(i)))
+
+        # Function to append a Ticket object to a result row
+        ticket_for_info = lambda r : Ticket(self.env,
+                ticket_id(r[cols.index('ticket')]))
+
+        ## Now process the results
+
+        # Add ticket objects to each row in the query result, 
+        # Note: fetchall() returns a list of tuples, so we have to
+        # convert those tuples to lists
+        # XXX: the cols bit sucks.
+        tlist = map(ticket_for_info, info)
+
+        # Create a dict from that list with ticket.id as the keys
+        tdict = {}
+        map(lambda t : tdict.setdefault(t.id, t), 
+                filter(ticket_in_gantt, tlist))
+
+        show_opened = self.env.config.getbool('gantt-charts',
+                'show_opened', 'false')
+
+        for i in range(len(info)):
+            row = info[i]
+
+            # If we get a KeyError, the ticket is not in tdict, because
+            # it is not checked to include in gantt charts.
+            try: 
+                ticket = tdict[row[cols.index('ticket')]]
+            except KeyError: 
+                continue
+
+            try:
+                # Get the due to start, due to end, open, and last
+                # change time for the ticket (this also takes into
+                # consideration dependencies times.)
+                start,end,open,changed = \
+                        self._dates_for_ticket(ticket, tdict)
+                
+                # Limit the summary to the max characters configured, or
+                # 16 chars in the gantt chart display.  We expose the
+                # full summary to the template, but it's not currently
+                # used.
+                try:
+                    sumlen = self.env.config.getint('gantt-charts',
+                            'summary_length', 16)
+                except AttributeError:
+                    sumlen = int(self.env.config.get('gantt-charts',
+                            'summary_length', 16))
+
+                summary = ticket.values['summary']
+
+                if len(summary) > sumlen:
+                    shortsum = "%s..." % summary[:16]
+                else:
+                    shortsum = summary
+                
+                tickets.append(
+                        {'id': ticket.id,
+                         'summary':summary,
+                         'shortsum':shortsum,
+                         'href': self.env.href.ticket(ticket.id),
+                         'start': start.toordinal(),
+                         'end': end.toordinal(),
+                         'open': open.toordinal(),
+                         'changed': changed.toordinal(),
+                         'color': row[cols.index("__color__")]
+                         })
+                       
+                if start not in dates: dates.append(start)
+                if end not in dates: dates.append(end)
+                if open not in dates and show_opened: 
+                    dates.append(open)
+
+            except Exception, e:
+                self.env.log.debug("Exception for ticket %s" % ticket.id)
+                self.env.log.debug(e)
+                broken.append(
+                        {'id': ticket.id,
+                         'href': self.env.href.ticket(ticket.id),
+                         'error':str(e)})
+
+        # Get the dates from the tdict, stored as both string values and
+        # ordinal values
+        # Catching a NameError if we're in python 2.3
+        try:
+            dates = [{'str':str(d), 'ord':d.toordinal()} \
+                    for d in sorted(dates)]
+        except NameError:
+            dates.sort()
+            dates = [{'str':str(d), 'ord':d.toordinal()} for d in dates]
+
+        # Using that dates list, set the spans of each ticket in the
+        # tickets list.
+        dlist = [d['ord'] for d in dates]
+        map(lambda t : \
+                t.setdefault('span', 
+                        1 + dlist.index(t['end']) - dlist.index(t['start'])),
+            tickets)
+        if show_opened:
+            map(lambda t : \
+                    t.setdefault('ospan', dlist.index(t['start']) 
+                                - dlist.index(t['open'])),
+                tickets)
+
+        return tickets,dates,broken
+
+    def _dates_for_ticket(self, ticket, tdict):
+        import locale
+
+        # XXX: Conf value
+        date_format = self.env.config.get('gantt-charts', 'date_format',
+                '%m/%d/%Y')
+
+        # Function to create date objects from date strings
+        date_for_string = lambda s,f : datetime.date.fromtimestamp(
+                time.mktime(time.strptime(s,f)))
+        date_from_date_by_num = lambda n,s : \
+            datetime.date.fromordinal(s.toordinal() + int(n))
+ 
+        depends = ticket.values.get('dependencies')
+
+        # Get the start date, if there is one, otherwise we'll find it
+        # later through dependencies
+        start_s = ticket.values.get('due_assign')
+        if start_s:
+            start = date_for_string(start_s, date_format)
+        else: 
+            start = None
+
+        # Cycle through the depends values, and set the start date
+        # appropriately if necessary, based on the due date of a dep
+        if depends:
+            for d in depends.split(","):
+                d = int(d.strip().strip("#"))
+
+                # If the dependency is not in the ticket dictionary,
+                # it's either closed or not included in gantt charts,
+                # therefore we ignore it.
+                if d not in tdict.keys(): continue
+
+                d_start,d_due,o,c = self._dates_for_ticket(tdict[d], tdict)
+                if not start or d_due > start: start = d_due
+
+        # The start date is optional when the ticket depends on another
+        # ticket, otherwise if it is None, we've got a problem.
+        #
+        # Unless explicitly disabled in the config file, use the
+        # creation date as the start date for this ticket.
+        if not start:
+            use_cdate = self.env.config.getbool("gantt-charts", 
+                    "use_creation_date", "true")
+
+            if use_cdate:
+                start = datetime.date.fromtimestamp(ticket.time_created)
+            else:
+                raise ValueError, "Couldn't get start date"
+
+        due_s = ticket.values.get('due_close')
+        if not due_s:
+            raise ValueError, "due date required for inclusion"
+        
+        # due_close can either be an integer for number of days from
+        # the start, or an actual date matching our format.  Try the
+        # date first, then if we get a value error (it doesn't match
+        # the format), try it as an integer
+        try: 
+            due = date_for_string(due_s, date_format)
+        except ValueError, e:
+            due = date_from_date_by_num(due_s, start)
+
+        # If the start date is greater than the due date, something
+        # is wrong, so we raise an error, which will mark this
+        # ticket as broken for gantt purposes
+        if start > due:
+            raise ValueError, \
+                    "Ticket #%s start date (%s) is after due date (%s)" \
+                    % (str(ticket.id), str(start), str(due))
+
+        # Finally the ticket itself's open and close dates
+        open = datetime.date.fromtimestamp(ticket.time_created)
+        changed = datetime.date.fromtimestamp(ticket.time_changed)
+
+        return (start, due, open, changed)
+
+
+    def _paginate_tickets(self, tickets, dates):
+        return tickets, dates

tracgantt/htdocs/gantt.css

+
+.gantt td, .gantt td.active, .gantt td.open { padding: 0.2em 0.1em; }
+
+.gantt td a { width: 100%; display: block; border: 0; }
+.gantt td.open a { background: transparent; border-color: #eed; color: #eed; 
+	font-style: italic;  text-align: right;
+	border-top: 0; border-left: 0; border-right: 0;
+	}
+.gantt td.open a:hover { background: transparent; color: #eed; }
+
+.gantt a.color1 { background: #fdc; color: #a22;
+	border: 1px solid #e88;  border-bottom: 5px solid #e88; }
+.gantt a.color2 { background: #ffb; color: #880;
+	border: 1px solid #eea;  border-bottom: 5px solid #eea; }
+.gantt a.color3 { background: #fbfbfb; color: #444; 
+	border: 1px solid #ddd;  border-bottom: 5px solid #ddd; }
+.gantt a.color4 { background: #e7ffff; color: #099;
+	border: 1px solid #cee;  border-bottom: 5px solid #cee; }
+.gantt a.color5 { background: #e7eeff; color: #469;
+	border: 1px solid #cde;  border-bottom: 5px solid #cde; }
+.gantt a.color6 { background: #f0f0f0; color: #888;
+	border: 1px solid #ddd;  border-bottom: 5px solid #ddd; }
+
+.gantt a.color1:hover { background: #fdc; color: #a22 }
+.gantt a.color2:hover { background: #ffb; color: #880 }
+.gantt a.color3:hover { background: #fbfbfb; color: #444 }
+.gantt a.color4:hover { background: #e7ffff; color: #099 }
+.gantt a.color5:hover { background: #e7eeff; color: #469 }
+.gantt a.color6:hover { background: #f0f0f0; color: #888 }
+

tracgantt/templates/gantt.cs

+<?cs include "header.cs"?>
+<?cs include "macros.cs"?>
+
+<div id="ctxtnav" class="nav">
+ <h2>Gantt Navigation</h2>
+ <ul><li class="first"><?cs
+   if:chrome.links.up.0.href ?><a href="<?cs
+    var:chrome.links.up.0.href ?>">Available Charts</a><?cs
+   else ?>Available Charts<?cs
+  /if ?></li>
+</div>
+
+<div id="content" class="gantt">
+
+<?cs if $gantt.id == -1 ?>
+	<h1>Gantt Charts</h1>
+	<p><?cs var:description ?></p>
+
+	<table class="listing tickets">
+	<thead>
+	<tr>
+	<?cs each col = cols ?>
+		<th>
+			<?cs var:col ?>
+		</th>
+	<?cs /each ?>
+	</tr>
+	</thead>
+
+	<?cs set idx = #0 ?>
+	<?cs each row = rows ?>
+		<tr>
+			<td><a href="<?cs var:row.href ?>">{<?cs var:row.report ?>}</a></td>
+			<td><a href="<?cs var:row.href ?>"><?cs var:row.title ?></a></td>
+		</tr>
+	<?cs /each ?>
+	</table>
+	
+	
+<?cs else ?>
+	<h1><?cs var:gantt.title ?> Gantt Chart</h1>
+	
+
+	<table class="listing tickets">
+	<thead>
+	<tr>
+	<th/>
+	<?cs each date = gantt.dates ?>
+		<th>
+			<?cs var:date.str ?>
+		</th>
+	<?cs /each ?>
+	</tr>
+	</thead>
+
+	<?cs set idx = #0 ?>
+	<?cs each ticket = gantt.tickets ?>
+		<?cs set rcolor='color'+$ticket.color ?>
+
+		<tr>
+			<td class="ticket">
+				<a title="View ticket" href="<?cs var:ticket.href ?>">
+					#<?cs var:ticket.id ?></a>
+			</td>
+			<?cs each date = gantt.dates ?>
+				<?cs if:date.ord == ticket.start ?>
+					<td	class="active" colspan="<?cs var:ticket.span ?>">
+						<a class="<?cs var:rcolor ?>"
+								href="<?cs var:ticket.href ?>">
+							#<?cs var:ticket.id ?> <?cs var:ticket.shortsum ?>
+						</a>
+					</td>
+				<?cs elif: gantt.show_opened &&
+						date.ord == ticket.open && date.ord < ticket.start ?>
+					<td class="open" colspan="<?cs var:ticket.ospan ?>">
+						<a class="<?cs var:rcolor ?>">
+							Opened <?cs var:date.str ?>
+						</a>
+					</td>
+				<?cs elif !gantt.show_opened && (date.ord < ticket.start || date.ord > ticket.end) ?>
+					<td/>
+				<?cs elif gantt.show_opened && (date.ord < ticket.open || date.ord > ticket.end) ?>
+					<td/>
+				<?cs /if ?>
+			<?cs /each ?>
+		</tr>
+
+		<?cs set idx = idx + #1 ?>
+	<?cs /each ?>
+	</table>
+
+	<?cs if $gantt.broken_no > 0 ?>	
+		<p><b>WARNING</b>: The following tickets had errors that prevented them from being included in the gantt chart:</p>
+		<ul>
+		<?cs each ticket = gantt.broken ?>
+			<li>
+				<a href="<?cs var:ticket.href ?>">#<?cs var:ticket.id ?></a>
+				- <?cs var:ticket.error ?>
+			</li>
+		<?cs /each ?>
+		</ul>
+	<?cs /if ?>
+<?cs /if ?>
+ 
+</div>
+<?cs include "footer.cs" ?>