Index: htdocs/css/releaselist.css
===================================================================
--- htdocs/css/releaselist.css	(revision 0)
+++ htdocs/css/releaselist.css	(revision 0)
@@ -0,0 +1,50 @@
+/* Styles for the releaselist view */
+ul.releases { margin: 2em 0 0; padding: 0 }
+li.releases{ list-style: none; margin-bottom: 4em }
+li.release .info { white-space: nowrap }
+li.release .info h2 {
+ background: #f7f7f7;
+ border-bottom: 1px solid #d7d7d7;
+ margin: 0;
+}
+li.release .info h2 :link, li.release .info h2 :visited {
+ color: #000;
+ display: block;
+ border-bottom: none;
+}
+li.release .info h2 :link:hover, li.release .info h2 :visited:hover {
+ color: #000;
+}
+li.release .info h2 em { color: #b00; font-style: normal }
+li.release .info .date {
+ color: #888;
+ font-size: 11px;
+ font-style: italic;
+ margin: 0;
+}
+li.release .info .progress { margin: 1em 1em 0; width: 40em; max-width: 80% }
+li.release .info dl {
+ font-size: 10px;
+ font-style: italic;
+ margin: 0 1em 2em;
+ white-space: nowrap;
+}
+li.release .info dt { display: inline; margin-left: .5em }
+li.release .info dd { display: inline; margin: 0 1em 0 .5em }
+li.release .descr { margin-left: 1em }
+
+/* Styles for the milestone view */
+.release .date { color: #888; font-style: italic }
+.release .descr { margin: 1em 0 2em }
+
+/* Release view preferences */
+#prefs fieldset { margin: 1em .5em .5em; padding: .5em 1em 0 }
+
+
+/* Styles for the release edit form */
+#edit fieldset { margin: 1em 0 }
+#edit em { color: #888; font-size: smaller }
+#edit .disabled em { color: #d7d7d7 }
+#edit .field { margin-top: 1.3em }
+#edit label { padding-left: .2em }
+#edit textarea#descr { width: 97% }
Index: htdocs/css/release.css
===================================================================
--- htdocs/css/release.css	(revision 0)
+++ htdocs/css/release.css	(revision 0)
@@ -0,0 +1,20 @@
+@import url(diff.css);
+
+/* Release overview */
+#overview .files { padding-top: 1em }
+#overview .files ul { margin: 0; padding: 0 }
+#overview .files li { list-style-type: none }
+#overview .files li .comment { display: none }
+#overview .files li div {
+ border: 1px solid #999;
+ float: left;
+ margin: .2em .5em 0 0;
+ overflow: hidden;
+ width: .8em; height: .8em;
+}
+#overview .message { padding: 1em 0 1px }
+#overview dd.message p, #overview dd.message ul, #overview dd.message ol {
+ margin-bottom: 1em;
+ margin-top: 0;
+}
+#overview .files { padding: 1px 0 }
Index: htdocs/css/timeline.css
===================================================================
--- htdocs/css/timeline.css	(revision 1087)
+++ htdocs/css/timeline.css	(working copy)
@@ -38,6 +38,7 @@
 dt.closedticket, dt.closedticket a { background-image: url(../closedticket.png) !important }
 dt.wiki, dt.wiki a { background-image: url(../wiki.png) !important }
 dt.milestone, dt.milestone a { background-image: url(../milestone.png) !important }
+dt.release, dt.release a { background-image: url(../release.png) !important }
 
 .diff-unmod { color: #000 }
 .diff-rem { color: #e00 }
Index: htdocs/release.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: htdocs/release.png
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Index: trac/core.py
===================================================================
--- trac/core.py	(revision 1087)
+++ trac/core.py	(working copy)
@@ -59,7 +59,9 @@
     'attachment'  : ('File', 'Attachment', 0),
     'roadmap'     : ('Roadmap', 'Roadmap', 0),
     'settings'    : ('Settings', 'Settings', 0),
-    'milestone'   : ('Milestone', 'Milestone', 0)
+    'milestone'   : ('Milestone', 'Milestone', 0),
+    'releaselist' : ('Releaselist', 'Releaselist', 0),
+    'release'     : ('Release', 'Release', 0)
     }
 
 class TracFieldStorage(cgi.FieldStorage):
@@ -93,7 +95,7 @@
         if match.group(2):
             set_if_missing(args, 'page', match.group(2))
         return args
-    match = re.search('^/(newticket|timeline|search|roadmap|settings|query)/?', path_info)
+    match = re.search('^/(newticket|timeline|releaselist|search|roadmap|settings|query)/?', path_info)
     if match:
         set_if_missing(args, 'mode', match.group(1))
         return args
@@ -127,6 +129,12 @@
         if match.group(1):
             set_if_missing(args, 'id', urllib.unquote_plus(match.group(1)))
         return args
+    match = re.search('^/release(?:/([^\?]+))?(?:/(.*)/?)?', path_info)
+    if match:
+        set_if_missing(args, 'mode', 'release')
+        if match.group(1):
+            set_if_missing(args, 'rev', urllib.unquote_plus(match.group(1)))
+        return args
     return args
 
 def parse_args(command, path_info, query_string,
@@ -221,6 +229,7 @@
     hdf.setValue('trac.href.browser', env.href.browser('/'))
     hdf.setValue('trac.href.timeline', env.href.timeline())
     hdf.setValue('trac.href.roadmap', env.href.roadmap())
+    hdf.setValue('trac.href.releaselist', env.href.releaselist())
     hdf.setValue('trac.href.report', env.href.report())
     hdf.setValue('trac.href.query', env.href.query())
     hdf.setValue('trac.href.newticket', env.href.newticket())
Index: trac/db_default.py
===================================================================
--- trac/db_default.py	(revision 1087)
+++ trac/db_default.py	(working copy)
@@ -21,7 +21,7 @@
 
 
 # Database version identifier. Used for automatic upgrades.
-db_version = 7
+db_version = 8
 
 def __mkreports(reps):
     """Utility function used to create report data in same syntax as the
@@ -40,6 +40,15 @@
 ##
 
 schema = """
+CREATE TABLE release (
+        rev             integer PRIMARY KEY,
+        name		text,
+        changesetby     text,
+        modifiedby      text,
+        time            integer,
+        timemodified    integer,
+        descr           text        
+);
 CREATE TABLE revision (
         rev             integer PRIMARY KEY,
         time            integer,
@@ -401,6 +410,8 @@
                 ('anonymous', 'BROWSER_VIEW'),
                 ('anonymous', 'TIMELINE_VIEW'),
                 ('anonymous', 'CHANGESET_VIEW'),
+                ('anonymous', 'RELEASELIST_VIEW'),
+                ('anonymous', 'RELEASE_VIEW'),
                 ('anonymous', 'ROADMAP_VIEW'),
                 ('anonymous', 'MILESTONE_VIEW'))),
            ('system',
Index: trac/perm.py
===================================================================
--- trac/perm.py	(revision 1087)
+++ trac/perm.py	(working copy)
@@ -30,6 +30,7 @@
 CHANGESET_VIEW = 'CHANGESET_VIEW'
 BROWSER_VIEW   = 'BROWSER_VIEW'
 ROADMAP_VIEW   = 'ROADMAP_VIEW'
+RELEASELIST_VIEW   = 'RELEASELIST_VIEW'
 
 TICKET_VIEW    = 'TICKET_VIEW'
 TICKET_CREATE  = 'TICKET_CREATE'
@@ -51,6 +52,11 @@
 MILESTONE_MODIFY = 'MILESTONE_MODIFY'
 MILESTONE_DELETE = 'MILESTONE_DELETE'
 
+RELEASE_VIEW = 'RELEASE_VIEW'
+RELEASE_CREATE = 'RELEASE_CREATE'
+RELEASE_MODIFY = 'RELEASE_MODIFY'
+RELEASE_DELETE = 'RELEASE_DELETE'
+
 AUTHZSVN_VIEW = 'AUTHZSVN_VIEW'
 AUTHZSVN_MODIFY = 'AUTHZSVN_MODIFY'
 
@@ -59,21 +65,22 @@
 REPORT_ADMIN = 'REPORT_ADMIN'
 WIKI_ADMIN = 'WIKI_ADMIN'
 ROADMAP_ADMIN = 'ROADMAP_ADMIN'
+RELEASE_ADMIN = 'RELEASE_ADMIN'
 AUTHZSVN_ADMIN = 'AUTHZSVN_ADMIN'
 
 meta_permission = {
-    TRAC_ADMIN: [TICKET_ADMIN, REPORT_ADMIN, WIKI_ADMIN, ROADMAP_ADMIN,
+    TRAC_ADMIN: [TICKET_ADMIN, REPORT_ADMIN, WIKI_ADMIN, RELEASE_ADMIN, ROADMAP_ADMIN,
                  TIMELINE_VIEW, SEARCH_VIEW, CONFIG_VIEW, LOG_VIEW,
                  FILE_VIEW, CHANGESET_VIEW, BROWSER_VIEW],
     TICKET_ADMIN: [TICKET_VIEW, TICKET_CREATE, TICKET_MODIFY],
     REPORT_ADMIN: [REPORT_VIEW, REPORT_SQL_VIEW, REPORT_CREATE, REPORT_MODIFY,
                    REPORT_DELETE],
     WIKI_ADMIN: [WIKI_VIEW, WIKI_CREATE, WIKI_MODIFY, WIKI_DELETE],
+    RELEASE_ADMIN: [RELEASELIST_VIEW, RELEASE_VIEW, RELEASE_CREATE, RELEASE_MODIFY, RELEASE_DELETE],
     ROADMAP_ADMIN: [ROADMAP_VIEW, MILESTONE_VIEW, MILESTONE_CREATE,
                     MILESTONE_MODIFY, MILESTONE_DELETE],
     AUTHZSVN_ADMIN: [AUTHZSVN_VIEW, AUTHZSVN_MODIFY]}
 
-
 class PermissionError (StandardError):
     """Insufficient permissions to complete the operation"""
     def __init__ (self, action):
Index: trac/Timeline.py
===================================================================
--- trac/Timeline.py	(revision 1087)
+++ trac/Timeline.py	(working copy)
@@ -35,15 +35,16 @@
     template_rss_name = 'timeline_rss.cs'
 
     def get_info (self, start, stop, maxrows, tickets,
-                  changeset, wiki, milestone):
+                  changeset, wiki, milestone, release):
         cursor = self.db.cursor ()
 
         tickets = tickets and self.perm.has_permission(perm.TICKET_VIEW)
         changeset = changeset and self.perm.has_permission(perm.CHANGESET_VIEW)
         wiki = wiki and self.perm.has_permission(perm.WIKI_VIEW)
         milestone = milestone and self.perm.has_permission(perm.MILESTONE_VIEW)
+        release = release and self.perm.has_permission(perm.RELEASE_VIEW)
 
-        if tickets == changeset == wiki == milestone == 0:
+        if tickets == changeset == wiki == milestone == release == 0:
             return []
 
         CHANGESET = 1
@@ -52,6 +53,7 @@
         REOPENED_TICKET = 4
         WIKI = 5
         MILESTONE = 6
+        RELEASE = 7
 
         q = []
         if changeset:
@@ -90,6 +92,11 @@
                      "name AS message, '' AS author " 
                      "FROM milestone WHERE time>=%s AND time<=%s" %
                      (start, stop))
+        if release:
+            q.append("SELECT time, rev AS idata, '' AS tdata, 7 AS type, "
+                     " name AS message, '' AS author "
+                     "FROM release WHERE time>=%s AND time<=%s" %
+                     (start, stop))
 
         q_str = string.join(q, ' UNION ALL ')
         q_str += ' ORDER BY time DESC'
@@ -168,6 +175,9 @@
             elif item['type'] == MILESTONE:
                 item['href'] = self.env.href.milestone(item['message'])
                 item['message'] = util.escape(item['message'])
+            elif item['type'] == RELEASE:
+                item['href'] = self.env.href.release(item['idata'])
+                item['message'] = util.escape(item['message'])
             else:               # TICKET
                 item['href'] = self.env.href.ticket(item['idata'])
                 msg = item['message']
@@ -223,8 +233,9 @@
         ticket = self.args.has_key('ticket')
         changeset = self.args.has_key('changeset')
         milestone = self.args.has_key('milestone')
-        if not (wiki or ticket or changeset or milestone):
-            wiki = ticket = changeset = milestone = 1
+        release = self.args.has_key('release')
+        if not (wiki or ticket or changeset or milestone or release):
+            wiki = ticket = changeset = milestone = release = 1
 
         if wiki:
             self.req.hdf.setValue('timeline.wiki', 'checked')
@@ -234,9 +245,11 @@
             self.req.hdf.setValue('timeline.changeset', 'checked')
         if milestone:
             self.req.hdf.setValue('timeline.milestone', 'checked')
+        if release:
+            self.req.hdf.setValue('timeline.release', 'checked')
 
         info = self.get_info (start, stop, maxrows, ticket,
-                              changeset, wiki, milestone)
+                              changeset, wiki, milestone, release)
         util.add_dictlist_to_hdf(info, self.req.hdf, 'timeline.items')
         self.req.hdf.setValue('title', 'Timeline')
 
Index: trac/Release.py
===================================================================
--- trac/Release.py	(revision 0)
+++ trac/Release.py	(revision 0)
@@ -0,0 +1,204 @@
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2003, 2004 Edgewall Software
+# Copyright (C) 2003, 2004 Jonas Borgström <jonas@edgewall.com>
+#
+# Trac is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or (at your option) any later version.
+#
+# Trac is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+# Author: James Moger <james.moger@transonic.com>
+# Based on milestone.py by:
+#          Christopher Lenz <cmlenz@gmx.de>
+
+import time
+
+from Module import Module
+from util import add_to_hdf, TracError
+from WikiFormatter import wiki_to_html
+import perm
+
+class Release(Module):
+    template_name = 'release.cs'
+
+    def save_release(self, rev):
+        self.perm.assert_permission(perm.RELEASE_MODIFY)
+        if self.args.has_key('save'):
+            name = self.args.get('name', '')
+            if not name:
+                raise TracError('You must provide a name for the release.',
+                                'Required Field Missing')
+	    cursor = self.db.cursor()
+            cursor.execute("SELECT time, author FROM revision "
+                "WHERE rev = %d", rev)
+            row = cursor.fetchone()
+            
+            date = row['time'] + 1
+            descr = self.args.get('descr', '')
+            changesetby = row['author']
+            modifiedby = self.req.authname
+
+            cursor.execute("SELECT name, time, descr FROM release "
+                "WHERE rev = %d ORDER BY time, name", rev)
+            row = cursor.fetchone()
+            cursor.close()
+            if not row:
+                self.create_release(rev, name, changesetby, modifiedby, date, descr)
+            else:
+                self.update_release(rev, name, modifiedby, descr)
+        elif rev:
+            self.req.redirect(self.env.href.release(rev))
+        else:
+            self.req.redirect(self.env.href.releaselist())
+
+    def create_release(self, rev, name, changesetby, modifiedby, date=0, descr=''):
+        self.perm.assert_permission(perm.RELEASE_CREATE)
+        if not name:
+        	name = 'unknown @ %d', rev
+        when = int(time.time())
+        
+        cursor = self.db.cursor()
+        self.log.debug("Creating new release '%s'" % name)
+        cursor.execute("INSERT INTO release (rev, name, changesetby, modifiedby, time, timemodified, descr) "
+                       "VALUES (%d, %s, %s, %s, %d, %d, %s)", rev, name, changesetby, modifiedby, date, when, descr)
+        self.db.commit()
+        self.req.redirect(self.env.href.release(rev))
+
+    def delete_release(self, rev):
+        self.perm.assert_permission(perm.RELEASE_DELETE)
+        release = self.get_release(rev)
+        if self.args.has_key('delete'):
+            cursor = self.db.cursor()
+            self.log.info('Deleting release %d' % rev)
+            cursor.execute("DELETE FROM release WHERE rev = %d", rev)
+            self.db.commit()
+            self.req.redirect(self.env.href.releaselist())
+        else:
+            self.req.redirect(self.env.href.release(rev))
+
+    def update_release(self, rev, name, modifiedby, descr):
+        self.perm.assert_permission(perm.RELEASE_MODIFY)
+        cursor = self.db.cursor()
+        self.log.info("Updating release '%d'" % rev)
+        
+        when = int(time.time())
+        
+        if self.args.has_key('save'):
+            cursor.execute("UPDATE release SET name = %s, timemodified = %d,"
+                           "descr = %s, modifiedby = %s WHERE rev = %d",
+                           name, when, descr, modifiedby, rev)
+            self.db.commit()
+            self.req.redirect(self.env.href.release(rev))
+        else:
+            self.req.redirect(self.env.href.release(rev))
+
+
+    def get_release(self, rev):
+        cursor = self.db.cursor()
+        cursor.execute("SELECT name, time, timemodified, changesetby, modifiedby, descr FROM release "
+                       "WHERE rev = %d ORDER BY time, name", rev)
+        row = cursor.fetchone()
+        cursor.close()
+        if not row:
+            raise TracError('Release %d does not exist.' % rev,
+                            'Invalid Release Number')
+        release = { 'name': row['name'] }
+        self.req.hdf.setValue('release.revision', str(self.rev))
+        if self.perm.has_permission(perm.CHANGESET_VIEW):
+	    self.req.hdf.setValue('release.href.changeset',
+                self.env.href.changeset(rev))
+            self.req.hdf.setValue('release.changesetby', row['changesetby'])
+        self.req.hdf.setValue('release.modifiedby', row['modifiedby'])
+        t = row['timemodified'] and int(row['timemodified'])
+        if t > 0:
+            self.req.hdf.setValue('release.timemodified', time.strftime('%x %X', time.localtime(t)))
+        descr = row['descr']
+        if descr:
+            release['descr_source'] = descr
+            release['descr'] = wiki_to_html(descr, self.req.hdf, self.env, self.db)
+        t = row['time'] and int(row['time'])
+        if t > 0:
+            release['date'] = time.strftime('%x %X', time.localtime(t))
+        return release
+
+    def render(self):
+        self.perm.assert_permission(perm.RELEASE_VIEW)
+
+        self.add_link('up', self.env.href.releaselist(), 'Releases')
+
+        action = self.args.get('action', 'view')
+  	if self.args.has_key('rev'):
+            self.rev = int(self.args.get('rev'))
+        else:
+            self.req.redirect(self.env.href.releaselist())
+
+        if action == 'new':
+            self.perm.assert_permission(perm.RELEASE_CREATE)
+            self.render_neweditor(self.rev)
+        elif action == 'edit':
+            self.perm.assert_permission(perm.RELEASE_MODIFY)
+            self.render_editor(self.rev)
+        elif action == 'delete':
+            self.perm.assert_permission(perm.RELEASE_DELETE)
+            self.render_confirm(self.rev)
+        elif action == 'commit_changes':
+            self.save_release(self.rev)
+        elif action == 'confirm_delete':
+            self.delete_release(self.rev)
+        else:
+            self.render_view(self.rev)
+
+    def render_confirm(self, rev):
+        release = self.get_release(rev)
+        self.req.hdf.setValue('title', 'Release %s' % release['name'])
+        self.req.hdf.setValue('release.mode', 'delete')        
+        add_to_hdf(release, self.req.hdf, 'release')
+
+        cursor = self.db.cursor()
+        cursor.execute("SELECT name FROM release "
+                       "WHERE name != '' ORDER BY name")
+        releases = []
+        release_no = 0
+        while 1:
+            row = cursor.fetchone()
+            if not row:
+                break
+            self.req.hdf.setValue('releases.%d' % release_no, row['name'])
+            release_no += 1
+        cursor.close()
+
+    def render_editor(self, rev):
+        release = self.get_release(rev)
+        self.req.hdf.setValue('title', 'Release %s' % release['name'])
+        self.req.hdf.setValue('release.mode', 'edit')
+        add_to_hdf(release, self.req.hdf, 'release')
+
+    def render_neweditor(self, rev):
+        release = { 'revision': rev, 'name': '', 'descr': '' }
+        self.req.hdf.setValue('title', 'New Release')
+        self.req.hdf.setValue('release.mode', 'new')
+        add_to_hdf(release, self.req.hdf, 'release')
+        
+    def render_view(self, rev):
+        if self.perm.has_permission(perm.RELEASE_DELETE):
+            self.req.hdf.setValue('release.href.delete',
+                                   self.env.href.release(rev, 'delete'))
+        if self.perm.has_permission(perm.RELEASE_MODIFY):
+            self.req.hdf.setValue('release.href.edit',
+                                   self.env.href.release(rev, 'edit'))
+
+        release = self.get_release(rev)
+        self.req.hdf.setValue('title', 'Release %s' % release['name'])
+        self.req.hdf.setValue('release.mode', 'view')
+        add_to_hdf(release, self.req.hdf, 'release')
+
Index: trac/upgrades/db8.py
===================================================================
--- trac/upgrades/db8.py	(revision 0)
+++ trac/upgrades/db8.py	(revision 0)
@@ -0,0 +1,17 @@
+sql = """
+-- Create release table and add default release view permissions
+CREATE TABLE release (
+        rev             integer PRIMARY KEY,
+        name		text,
+        changesetby     text,
+        modifiedby      text,
+        time            integer,
+        timemodified    integer,
+        descr           text        
+);
+INSERT INTO permission (username, action) VALUES ('anonymous', 'RELEASELIST_VIEW');
+INSERT INTO permission (username, action) VALUES ('anonymous', 'RELEASE_VIEW');
+"""
+
+def do_upgrade(env, ver, cursor):
+    cursor.execute(sql)
Index: trac/Href.py
===================================================================
--- trac/Href.py	(revision 1087)
+++ trac/Href.py	(working copy)
@@ -116,6 +116,22 @@
             href += '?' + urllib.urlencode(params)
         return href
 
+    def releaselist(self):
+        href = href_join(self.base, 'releaselist')
+        return href
+
+    def release(self, rev, action=None):
+        params = []
+        if rev:
+             href = href_join(self.base, 'release', str(rev))
+        else:
+            href = href_join(self.base, 'release')
+        if action:
+            params.append(('action', action))
+        if params:
+            href += '?' + urllib.urlencode(params)
+        return href
+
     def settings(self):
         return href_join(self.base, 'settings')
 
Index: trac/Changeset.py
===================================================================
--- trac/Changeset.py	(revision 1087)
+++ trac/Changeset.py	(working copy)
@@ -153,6 +153,13 @@
     fs_ptr = None
     pool = None
 
+    def get_release_info (self, rev):
+        cursor = self.db.cursor ()
+        cursor.execute ('SELECT time, name, descr FROM release ' +
+                        'WHERE rev=%d', rev)
+        row = cursor.fetchone()
+        return row
+
     def get_changeset_info (self, rev):
         cursor = self.db.cursor ()
         cursor.execute ('SELECT time, author, message FROM revision ' +
@@ -194,6 +201,16 @@
         if self.args.has_key('update'):
             self.req.redirect(self.env.href.changeset(self.rev))
 
+        if self.perm.has_permission(perm.RELEASE_VIEW):
+	   release_info = self.get_release_info(self.rev)
+	   if release_info > 1:
+	      self.add_link('existingrelease', self.env.href.release(self.rev),
+           	'Show Tagged Release: %s' % release_info['name'])
+	   else:
+	      if self.perm.has_permission(perm.RELEASE_CREATE):
+                 self.add_link('newrelease', self.env.href.release(self.rev, 'new'),
+                    'Tag Changeset %d as New Release' % self.rev)
+	
         change_info = self.get_change_info (self.rev)
         changeset_info = self.get_changeset_info (self.rev)
 
Index: trac/Releaselist.py
===================================================================
--- trac/Releaselist.py	(revision 0)
+++ trac/Releaselist.py	(revision 0)
@@ -0,0 +1,69 @@
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2004 Edgewall Software
+# Copyright (C) 2004 Christopher Lenz <cmlenz@gmx.de>
+#
+# Trac is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or (at your option) any later version.
+#
+# Trac is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+# Author: James Moger <james.moger@transonic.com>
+# Based on roadmap.py by:
+#          Christopher Lenz <cmlenz@gmx.de>
+
+import re
+from time import localtime, strftime, time
+
+from __init__ import __version__
+import perm
+from util import add_to_hdf, CRLF, TracError
+import util
+from Module import Module
+from Wiki import wiki_to_html, wiki_to_oneliner
+
+
+class Releaselist(Module):
+    template_name = 'releaselist.cs'
+
+    def render(self):
+        self.perm.assert_permission(perm.RELEASELIST_VIEW)
+        self.req.hdf.setValue('title', 'Releases')
+
+        query = "SELECT name, time, descr, rev FROM release WHERE name != '' ORDER BY name ASC, time DESC"
+
+        cursor = self.db.cursor()
+        cursor.execute(query)
+        self.releases = []
+        while 1:
+            row = cursor.fetchone()
+            if not row:
+                break
+            release = {
+                'name': row['name'],
+                'href': self.env.href.release(row['rev']),
+                'time': row['time'] and int(row['time'])
+            }
+            descr = row['descr']
+
+            if descr:
+                shorttip = util.shorten_line(util.wiki_escape_newline(descr))
+                shortertip = wiki_to_oneliner(util.wiki_escape_newline(shorttip), self.req.hdf,
+                                                  self.env, self.db)
+                release['tooltip'] = util.escape(shortertip)
+            if release['time'] > 0:
+                release['date'] = strftime('%x %X', localtime(release['time']))
+            self.releases.append(release)
+        cursor.close()
+        add_to_hdf(self.releases, self.req.hdf, 'releaselist.releases')
+
+
Index: templates/releaselist.cs
===================================================================
--- templates/releaselist.cs	(revision 0)
+++ templates/releaselist.cs	(revision 0)
@@ -0,0 +1,27 @@
+<?cs set:html.stylesheet = 'css/releaselist.css' ?>
+<?cs include "header.cs"?>
+<?cs include "macros.cs"?>
+
+<div id="ctxtnav" class="nav">
+</div>
+
+<div id="content" class="releaselist">
+<h1>Releases</h1>
+
+ <ul class="releaselist"><?cs each:release = releaselist.releases ?>
+  <li class="release">
+   <div class="info">
+    <h2><a href="<?cs var:release.href ?>" title="<?cs var:release.tooltip ?>"><em><?cs
+      var:release.name ?></em></a></h2>
+    <p class="date"><?cs if:release.date ?>
+     <?cs var:release.date ?><?cs else ?>No date set<?cs /if ?>
+    </p>
+   </div>
+  </li>
+ <?cs /each ?></ul>
+ <div id="help">
+  <strong>Note:</strong> See <a href="<?cs
+    var:trac.href.wiki ?>/TracReleaselist">TracReleaselist</a> for help on using the release list.
+ </div>
+</div>
+<?cs include "footer.cs"?>
Index: templates/release.cs
===================================================================
--- templates/release.cs	(revision 0)
+++ templates/release.cs	(revision 0)
@@ -0,0 +1,89 @@
+<?cs set:html.stylesheet = 'css/releaselist.css' ?>
+<?cs include:"header.cs"?>
+<?cs include:"macros.cs"?>
+
+<div id="ctxtnav" class="nav">
+ <ul>
+  <?cs if:release.href.edit ?><li class="first"><a href="<?cs
+    var:release.href.edit ?>">Edit Release Info</a></li><?cs /if ?>
+  <?cs if:release.href.delete ?><li class="last"><a href="<?cs
+    var:release.href.delete ?>">Delete Release</a></li><?cs /if ?>
+ </ul>
+</div>
+
+<div id="content" class="release">
+ <?cs if:release.mode == "new" ?>
+ <h1>New Release</h1>
+ <?cs elif:release.mode == "edit" ?>
+ <h1>Edit Release <?cs var:release.name ?></h1>
+ <?cs elif:release.mode == "delete" ?>
+ <h1>Delete Release <?cs var:release.name ?></h1>
+ <?cs else ?>
+ <h1>Release <?cs var:release.name ?></h1>
+ <?cs /if ?>
+
+ <?cs if:release.mode == "edit" || release.mode == "new" ?>
+  <script type="text/javascript">
+    addEvent(window, 'load', function() {
+      document.getElementById('name').focus() }
+    );
+  </script>
+  <form id="edit" action="<?cs var:cgi_location ?>" method="post">
+   <input type="hidden" name="mode" value="release" />
+   <input type="hidden" name="rev" value="<?cs var:release.revision ?>" />
+   <input type="hidden" name="action" value="commit_changes" />
+   <div class="field">
+    <label for="name">Name of the release:</label><br />
+    <input type="text" id="name" name="name" size="32" value="<?cs
+      var:release.name ?>" />
+   </div>
+   <div class="field">
+    <fieldset class="iefix">
+     <label for="descr">Description (you may use <a tabindex="42" href="<?cs
+       var:trac.href.wiki ?>/WikiFormatting">WikiFormatting</a> here):</label>
+     <p><textarea id="descr" name="descr" rows="12" cols="80"><?cs
+       var:release.descr_source ?></textarea></p>
+     <?cs call:wiki_toolbar('descr') ?>
+    </fieldset>
+   </div>
+   <div class="buttons">
+    <?cs if:release.mode == "new"
+     ?><input type="submit" name="save" value="Add Release" /><?cs
+    else
+     ?><input type="submit" name="save" value="Save Changes" /><?cs
+    /if ?>
+    <input type="submit" name="cancel" value="Cancel" />
+   </div>
+  </form>
+ <?cs elif:release.mode == "delete" ?>
+  <form action="<?cs var:cgi_location ?>" method="post">
+   <input type="hidden" name="mode" value="release" />
+   <input type="hidden" name="rev" value="<?cs var:release.revision ?>" />
+   <input type="hidden" name="action" value="confirm_delete" />
+   <p><strong>Are you sure you want to delete this release?</strong></p>
+   <div class="buttons">
+    <input type="submit" name="cancel" value="Cancel" />
+    <input type="submit" name="delete" value="Delete Release" />
+   </div>
+  </form>
+ <?cs else ?>
+  <em class="date"><?cs if:release.date ?>
+   <?cs var:release.date ?><?cs else ?>No date specified<?cs /if ?>
+  </em>
+  <?cs if:release.href.changeset ?>
+  <dt class="date">Based on changeset <a class="prev" href="<?cs var:release.href.changeset ?>" title="Changeset <?cs var:release.revision ?>"><?cs var:release.revision ?></a> by <?cs var:release.changesetby ?></dt>
+  <?cs /if ?>
+  <dt class="date">Last changed by <?cs var:release.modifiedby ?> @ <?cs var:release.timemodified ?></dt>
+  <div class="descr"><?cs var:release.descr ?></div>
+ <?cs /if ?>
+
+ <?cs if:release.mode == "view" ?>
+<?cs /if ?>
+
+ <div id="help">
+  <strong>Note:</strong> See <a href="<?cs
+    var:trac.href.wiki ?>/TracReleaselist">TracReleaselist</a> for help on using the release list.
+ </div>
+</div>
+<?cs include:"footer.cs"?>
+
Index: templates/header.cs
===================================================================
--- templates/header.cs	(revision 1087)
+++ templates/header.cs	(working copy)
@@ -100,10 +100,13 @@
   set:$roadmap_view="roadmap" ?><?cs 
  /if ?>
 
+
 <div id="mainnav" class="nav">
  <ul>
   <?cs call:navlink("Wiki", $trac.href.wiki, $wiki_view,
                     $trac.acl.WIKI_VIEW, "1") ?>
+  <?cs call:navlink("Releases", $trac.href.releaselist, "releaselist",
+                    $trac.acl.RELEASELIST_VIEW, "") ?>
   <?cs call:navlink("Timeline", $trac.href.timeline, "timeline",
                     $trac.acl.TIMELINE_VIEW, "2") ?>
   <?cs call:navlink("Roadmap", trac.href.roadmap, $roadmap_view,
Index: templates/changeset.cs
===================================================================
--- templates/changeset.cs	(revision 1087)
+++ templates/changeset.cs	(working copy)
@@ -5,6 +5,18 @@
 <div id="ctxtnav" class="nav">
  <h2>Changeset Navigation</h2>
  <ul><?cs
+  if:len(links.newrelease) ?>
+  <li>
+   <a class="next" href="<?cs var:links.newrelease.0.href ?>" title="<?cs
+    var:links.newrelease.0.title ?>">Tag as New Release</a>
+   </li><?cs
+  /if ?><?cs
+  if:len(links.existingrelease) ?>
+  <li>
+   <a class="next" href="<?cs var:links.existingrelease.0.href ?>" title="<?cs
+    var:links.existingrelease.0.title ?>">Show Tagged Release</a>
+   </li><?cs
+  /if ?><?cs
   if:len(links.prev) ?>
    <li class="first<?cs if:!len(links.next) ?> last<?cs /if ?>">
     <a class="prev" href="<?cs var:links.prev.0.href ?>" title="<?cs
Index: templates/timeline.cs
===================================================================
--- templates/timeline.cs	(revision 1087)
+++ templates/timeline.cs	(working copy)
@@ -35,6 +35,11 @@
       if:timeline.milestone ?>checked="checked"<?cs /if ?> />
     <label for="milestone">Milestones</label>
    </div><?cs /if ?>
+   <?cs if:trac.acl.RELEASE_VIEW ?><div class="field">
+    <input type="checkbox" id="release" name="release" <?cs
+      if:timeline.release ?>checked="checked"<?cs /if ?> />
+    <label for="release">Releases</label>
+   </div><?cs /if ?>
   </fieldset>
   <div class="buttons">
    <input type="submit" value="Update" />
@@ -84,6 +89,9 @@
  <?cs elif:item.type == #6 ?><!-- milestone -->
   <?cs call:tlitem(item.href, 'milestone',
     '<em>Milestone '+$item.message+'</em> reached', '') ?>
+ <?cs elif:item.type == #7 ?><!-- release -->
+  <?cs call:tlitem(item.href, 'release',
+    'Release <em>'+$item.message +'</em>', '') ?>
  <?cs /if ?>
 <?cs /each ?>
 <?cs if:len(timeline.items) ?></dl><?cs /if ?>

