diff examples/trac/trac/ticket/model.py @ 39:93b4dcbafd7b trunk

Copy Trac to main branch.
author cmlenz
date Mon, 03 Jul 2006 18:53:27 +0000
parents
children
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/ticket/model.py
@@ -0,0 +1,765 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2006 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+
+import time
+import sys
+import re
+
+from trac.core import TracError
+from trac.ticket import TicketSystem
+from trac.util import sorted, embedded_numbers
+
+__all__ = ['Ticket', 'Type', 'Status', 'Resolution', 'Priority', 'Severity',
+           'Component', 'Milestone', 'Version']
+
+
+class Ticket(object):
+
+    def __init__(self, env, tkt_id=None, db=None):
+        self.env = env
+        self.fields = TicketSystem(self.env).get_ticket_fields()
+        self.values = {}
+        if tkt_id:
+            self._fetch_ticket(tkt_id, db)
+        else:
+            self._init_defaults(db)
+            self.id = self.time_created = self.time_changed = None
+        self._old = {}
+
+    def _get_db(self, db):
+        return db or self.env.get_db_cnx()
+
+    def _get_db_for_write(self, db):
+        if db:
+            return (db, False)
+        else:
+            return (self.env.get_db_cnx(), True)
+
+    exists = property(fget=lambda self: self.id is not None)
+
+    def _init_defaults(self, db=None):
+        for field in self.fields:
+            default = None
+            if not field.get('custom'):
+                default = self.env.config.get('ticket',
+                                              'default_' + field['name'])
+            else:
+                default = field.get('value')
+                options = field.get('options')
+                if default and options and default not in options:
+                    try:
+                        default_idx = int(default)
+                        if default_idx > len(options):
+                            raise ValueError
+                        default = options[default_idx]
+                    except ValueError:
+                        self.env.log.warning('Invalid default value for '
+                                             'custom field "%s"'
+                                             % field['name'])
+            if default:
+                self.values.setdefault(field['name'], default)
+
+    def _fetch_ticket(self, tkt_id, db=None):
+        db = self._get_db(db)
+
+        # Fetch the standard ticket fields
+        std_fields = [f['name'] for f in self.fields if not f.get('custom')]
+        cursor = db.cursor()
+        cursor.execute("SELECT %s,time,changetime FROM ticket WHERE id=%%s"
+                       % ','.join(std_fields), (tkt_id,))
+        row = cursor.fetchone()
+        if not row:
+            raise TracError('Ticket %d does not exist.' % tkt_id,
+                            'Invalid Ticket Number')
+
+        self.id = tkt_id
+        for i in range(len(std_fields)):
+            self.values[std_fields[i]] = row[i] or ''
+        self.time_created = row[len(std_fields)]
+        self.time_changed = row[len(std_fields) + 1]
+
+        # Fetch custom fields if available
+        custom_fields = [f['name'] for f in self.fields if f.get('custom')]
+        cursor.execute("SELECT name,value FROM ticket_custom WHERE ticket=%s",
+                       (tkt_id,))
+        for name, value in cursor:
+            if name in custom_fields:
+                self.values[name] = value
+
+    def __getitem__(self, name):
+        return self.values[name]
+
+    def __setitem__(self, name, value):
+        """Log ticket modifications so the table ticket_change can be updated"""
+        if self.values.has_key(name) and self.values[name] == value:
+            return
+        if not self._old.has_key(name): # Changed field
+            self._old[name] = self.values.get(name)
+        elif self._old[name] == value: # Change of field reverted
+            del self._old[name]
+        if value:
+            field = [field for field in self.fields if field['name'] == name]
+            if field and field[0].get('type') != 'textarea':
+                value = value.strip()
+        self.values[name] = value
+
+    def populate(self, values):
+        """Populate the ticket with 'suitable' values from a dictionary"""
+        field_names = [f['name'] for f in self.fields]
+        for name in [name for name in values.keys() if name in field_names]:
+            self[name] = values.get(name, '')
+
+        # We have to do an extra trick to catch unchecked checkboxes
+        for name in [name for name in values.keys() if name[9:] in field_names
+                     and name.startswith('checkbox_')]:
+            if not values.has_key(name[9:]):
+                self[name[9:]] = '0'
+
+    def insert(self, when=0, db=None):
+        """Add ticket to database"""
+        assert not self.exists, 'Cannot insert an existing ticket'
+        db, handle_ta = self._get_db_for_write(db)
+
+        # Add a timestamp
+        if not when:
+            when = int(time.time())
+        self.time_created = self.time_changed = when
+
+        cursor = db.cursor()
+
+        # The owner field defaults to the component owner
+        if self.values.get('component') and not self.values.get('owner'):
+            try:
+                component = Component(self.env, self['component'], db=db)
+                if component.owner:
+                    self['owner'] = component.owner
+            except TracError, e:
+                # Assume that no such component exists
+                pass
+
+        # Insert ticket record
+        std_fields = [f['name'] for f in self.fields if not f.get('custom')
+                      and self.values.has_key(f['name'])]
+        cursor.execute("INSERT INTO ticket (%s,time,changetime) VALUES (%s)"
+                       % (','.join(std_fields),
+                          ','.join(['%s'] * (len(std_fields) + 2))),
+                       [self[name] for name in std_fields] +
+                       [self.time_created, self.time_changed])
+        tkt_id = db.get_last_id(cursor, 'ticket')
+
+        # Insert custom fields
+        custom_fields = [f['name'] for f in self.fields if f.get('custom')
+                         and self.values.has_key(f['name'])]
+        if custom_fields:
+            cursor.executemany("INSERT INTO ticket_custom (ticket,name,value) "
+                               "VALUES (%s,%s,%s)", [(tkt_id, name, self[name])
+                                                     for name in custom_fields])
+
+        if handle_ta:
+            db.commit()
+
+        self.id = tkt_id
+        self._old = {}
+
+        for listener in TicketSystem(self.env).change_listeners:
+            listener.ticket_created(self)
+
+        return self.id
+
+    def save_changes(self, author, comment, when=0, db=None, cnum=''):
+        """
+        Store ticket changes in the database. The ticket must already exist in
+        the database.
+        """
+        assert self.exists, 'Cannot update a new ticket'
+
+        if not self._old and not comment:
+            return # Not modified
+
+        db, handle_ta = self._get_db_for_write(db)
+        cursor = db.cursor()
+        when = int(when or time.time())
+
+        if self.values.has_key('component'):
+            # If the component is changed on a 'new' ticket then owner field
+            # is updated accordingly. (#623).
+            if self.values.get('status') == 'new' \
+                    and self._old.has_key('component') \
+                    and not self._old.has_key('owner'):
+                try:
+                    old_comp = Component(self.env, self._old['component'], db)
+                    old_owner = old_comp.owner or ''
+                    current_owner = self.values.get('owner') or ''
+                    if old_owner == current_owner:
+                        new_comp = Component(self.env, self['component'], db)
+                        self['owner'] = new_comp.owner
+                except TracError, e:
+                    # If the old component has been removed from the database we
+                    # just leave the owner as is.
+                    pass
+
+        # Fix up cc list separators and remove duplicates
+        if self.values.has_key('cc'):
+            cclist = []
+            for cc in re.split(r'[;,\s]+', self.values['cc']):
+                if cc not in cclist:
+                    cclist.append(cc)
+            self.values['cc'] = ', '.join(cclist)
+
+        custom_fields = [f['name'] for f in self.fields if f.get('custom')]
+        for name in self._old.keys():
+            if name in custom_fields:
+                cursor.execute("SELECT * FROM ticket_custom " 
+                               "WHERE ticket=%s and name=%s", (self.id, name))
+                if cursor.fetchone():
+                    cursor.execute("UPDATE ticket_custom SET value=%s "
+                                   "WHERE ticket=%s AND name=%s",
+                                   (self[name], self.id, name))
+                else:
+                    cursor.execute("INSERT INTO ticket_custom (ticket,name,"
+                                   "value) VALUES(%s,%s,%s)",
+                                   (self.id, name, self[name]))
+            else:
+                cursor.execute("UPDATE ticket SET %s=%%s WHERE id=%%s" % name,
+                               (self[name], self.id))
+            cursor.execute("INSERT INTO ticket_change "
+                           "(ticket,time,author,field,oldvalue,newvalue) "
+                           "VALUES (%s, %s, %s, %s, %s, %s)",
+                           (self.id, when, author, name, self._old[name],
+                            self[name]))
+        # always save comment, even if empty (numbering support for timeline)
+        cursor.execute("INSERT INTO ticket_change "
+                       "(ticket,time,author,field,oldvalue,newvalue) "
+                       "VALUES (%s,%s,%s,'comment',%s,%s)",
+                       (self.id, when, author, cnum, comment))
+
+        cursor.execute("UPDATE ticket SET changetime=%s WHERE id=%s",
+                       (when, self.id))
+
+        if handle_ta:
+            db.commit()
+        self._old = {}
+        self.time_changed = when
+
+        for listener in TicketSystem(self.env).change_listeners:
+            listener.ticket_changed(self, comment, self._old)
+
+    def get_changelog(self, when=0, db=None):
+        """Return the changelog as a list of tuples of the form
+        (time, author, field, oldvalue, newvalue, permanent).
+
+        While the other tuple elements are quite self-explanatory,
+        the `permanent` flag is used to distinguish collateral changes
+        that are not yet immutable (like attachments, currently).
+        """
+        db = self._get_db(db)
+        cursor = db.cursor()
+        if when:
+            cursor.execute("SELECT time,author,field,oldvalue,newvalue,1 "
+                           "FROM ticket_change WHERE ticket=%s AND time=%s "
+                           "UNION "
+                           "SELECT time,author,'attachment',null,filename,0 "
+                           "FROM attachment WHERE id=%s AND time=%s "
+                           "UNION "
+                           "SELECT time,author,'comment',null,description,0 "
+                           "FROM attachment WHERE id=%s AND time=%s "
+                           "ORDER BY time",
+                           (self.id, when, str(self.id), when, self.id, when))
+        else:
+            cursor.execute("SELECT time,author,field,oldvalue,newvalue,1 "
+                           "FROM ticket_change WHERE ticket=%s "
+                           "UNION "
+                           "SELECT time,author,'attachment',null,filename,0 "
+                           "FROM attachment WHERE id=%s "
+                           "UNION "
+                           "SELECT time,author,'comment',null,description,0 "
+                           "FROM attachment WHERE id=%s "
+                           "ORDER BY time", (self.id,  str(self.id), self.id))
+        log = []
+        for t, author, field, oldvalue, newvalue, permanent in cursor:
+            log.append((int(t), author, field, oldvalue or '', newvalue or '',
+                        permanent))
+        return log
+
+    def delete(self, db=None):
+        db, handle_ta = self._get_db_for_write(db)
+        cursor = db.cursor()
+        cursor.execute("DELETE FROM ticket WHERE id=%s", (self.id,))
+        cursor.execute("DELETE FROM ticket_change WHERE ticket=%s", (self.id,))
+        cursor.execute("DELETE FROM attachment "
+                       " WHERE type='ticket' and id=%s", (self.id,))
+        cursor.execute("DELETE FROM ticket_custom WHERE ticket=%s", (self.id,))
+
+        if handle_ta:
+            db.commit()
+
+        for listener in TicketSystem(self.env).change_listeners:
+            listener.ticket_deleted(self)
+
+
+class AbstractEnum(object):
+    type = None
+    ticket_col = None
+
+    def __init__(self, env, name=None, db=None):
+        if not self.ticket_col:
+            self.ticket_col = self.type
+        self.env = env
+        if name:
+            if not db:
+                db = self.env.get_db_cnx()
+            cursor = db.cursor()
+            cursor.execute("SELECT value FROM enum WHERE type=%s AND name=%s",
+                           (self.type, name))
+            row = cursor.fetchone()
+            if not row:
+                raise TracError, '%s %s does not exist.' % (self.type, name)
+            self.value = self._old_value = row[0]
+            self.name = self._old_name = name
+        else:
+            self.value = self._old_value = None
+            self.name = self._old_name = None
+
+    exists = property(fget=lambda self: self._old_value is not None)
+
+    def delete(self, db=None):
+        assert self.exists, 'Cannot deleting non-existent %s' % self.type
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.info('Deleting %s %s' % (self.type, self.name))
+        cursor.execute("DELETE FROM enum WHERE type=%s AND value=%s",
+                       (self.type, self._old_value))
+
+        if handle_ta:
+            db.commit()
+        self.value = self._old_value = None
+        self.name = self._old_name = None
+
+    def insert(self, db=None):
+        assert not self.exists, 'Cannot insert existing %s' % self.type
+        assert self.name, 'Cannot create %s with no name' % self.type
+        self.name = self.name.strip()
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.debug("Creating new %s '%s'" % (self.type, self.name))
+        if not self.value:
+            cursor.execute(("SELECT COALESCE(MAX(%s),0) FROM enum "
+                            "WHERE type=%%s") % db.cast('value', 'int'),
+                           (self.type,))
+            self.value = int(float(cursor.fetchone()[0])) + 1
+        cursor.execute("INSERT INTO enum (type,name,value) VALUES (%s,%s,%s)",
+                       (self.type, self.name, self.value))
+
+        if handle_ta:
+            db.commit()
+        self._old_name = self.name
+        self._old_value = self.value
+
+    def update(self, db=None):
+        assert self.exists, 'Cannot update non-existent %s' % self.type
+        assert self.name, 'Cannot update %s with no name' % self.type
+        self.name = self.name.strip()
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.info('Updating %s "%s"' % (self.type, self.name))
+        cursor.execute("UPDATE enum SET name=%s,value=%s "
+                       "WHERE type=%s AND name=%s",
+                       (self.name, self.value, self.type, self._old_name))
+        if self.name != self._old_name:
+            # Update tickets
+            cursor.execute("UPDATE ticket SET %s=%%s WHERE %s=%%s" %
+                           (self.ticket_col, self.ticket_col),
+                           (self.name, self._old_name))
+
+        if handle_ta:
+            db.commit()
+        self._old_name = self.name
+        self._old_value = self.value
+
+    def select(cls, env, db=None):
+        if not db:
+            db = env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT name,value FROM enum WHERE type=%s "
+                       "ORDER BY value", (cls.type,))
+        for name, value in cursor:
+            obj = cls(env)
+            obj.name = obj._old_name = name
+            obj.value = obj._old_value = value
+            yield obj
+    select = classmethod(select)
+
+
+class Type(AbstractEnum):
+    type = 'ticket_type'
+    ticket_col = 'type'
+
+
+class Status(AbstractEnum):
+    type = 'status'
+
+
+class Resolution(AbstractEnum):
+    type = 'resolution'
+
+
+class Priority(AbstractEnum):
+    type = 'priority'
+
+
+class Severity(AbstractEnum):
+    type = 'severity'
+
+
+class Component(object):
+
+    def __init__(self, env, name=None, db=None):
+        self.env = env
+        if name:
+            if not db:
+                db = self.env.get_db_cnx()
+            cursor = db.cursor()
+            cursor.execute("SELECT owner,description FROM component "
+                           "WHERE name=%s", (name,))
+            row = cursor.fetchone()
+            if not row:
+                raise TracError, 'Component %s does not exist.' % name
+            self.name = self._old_name = name
+            self.owner = row[0] or None
+            self.description = row[1] or ''
+        else:
+            self.name = self._old_name = None
+            self.owner = None
+            self.description = None
+
+    exists = property(fget=lambda self: self._old_name is not None)
+
+    def delete(self, db=None):
+        assert self.exists, 'Cannot deleting non-existent component'
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.info('Deleting component %s' % self.name)
+        cursor.execute("DELETE FROM component WHERE name=%s", (self.name,))
+
+        self.name = self._old_name = None
+
+        if handle_ta:
+            db.commit()
+
+    def insert(self, db=None):
+        assert not self.exists, 'Cannot insert existing component'
+        assert self.name, 'Cannot create component with no name'
+        self.name = self.name.strip()
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.debug("Creating new component '%s'" % self.name)
+        cursor.execute("INSERT INTO component (name,owner,description) "
+                       "VALUES (%s,%s,%s)",
+                       (self.name, self.owner, self.description))
+
+        if handle_ta:
+            db.commit()
+
+    def update(self, db=None):
+        assert self.exists, 'Cannot update non-existent component'
+        assert self.name, 'Cannot update component with no name'
+        self.name = self.name.strip()
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.info('Updating component "%s"' % self.name)
+        cursor.execute("UPDATE component SET name=%s,owner=%s,description=%s "
+                       "WHERE name=%s",
+                       (self.name, self.owner, self.description,
+                        self._old_name))
+        if self.name != self._old_name:
+            # Update tickets
+            cursor.execute("UPDATE ticket SET component=%s WHERE component=%s",
+                           (self.name, self._old_name))
+            self._old_name = self.name
+
+        if handle_ta:
+            db.commit()
+
+    def select(cls, env, db=None):
+        if not db:
+            db = env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT name,owner,description FROM component "
+                       "ORDER BY name")
+        for name, owner, description in cursor:
+            component = cls(env)
+            component.name = name
+            component.owner = owner or None
+            component.description = description or ''
+            yield component
+    select = classmethod(select)
+
+
+class Milestone(object):
+
+    def __init__(self, env, name=None, db=None):
+        self.env = env
+        if name:
+            self._fetch(name, db)
+            self._old_name = name
+        else:
+            self.name = self._old_name = None
+            self.due = self.completed = 0
+            self.description = ''
+
+    def _fetch(self, name, db=None):
+        if not db:
+            db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT name,due,completed,description "
+                       "FROM milestone WHERE name=%s", (name,))
+        row = cursor.fetchone()
+        if not row:
+            raise TracError('Milestone %s does not exist.' % name,
+                            'Invalid Milestone Name')
+        self.name = row[0]
+        self.due = row[1] and int(row[1]) or 0
+        self.completed = row[2] and int(row[2]) or 0
+        self.description = row[3] or ''
+
+    exists = property(fget=lambda self: self._old_name is not None)
+    is_completed = property(fget=lambda self: self.completed != 0)
+    is_late = property(fget=lambda self: self.due and \
+                                         self.due < time.time() - 86400)
+
+    def delete(self, retarget_to=None, author=None, db=None):
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.info('Deleting milestone %s' % self.name)
+        cursor.execute("DELETE FROM milestone WHERE name=%s", (self.name,))
+
+        # Retarget/reset tickets associated with this milestone
+        now = time.time()
+        cursor.execute("SELECT id FROM ticket WHERE milestone=%s", (self.name,))
+        tkt_ids = [int(row[0]) for row in cursor]
+        for tkt_id in tkt_ids:
+            ticket = Ticket(self.env, tkt_id, db)
+            ticket['milestone'] = retarget_to
+            ticket.save_changes(author, 'Milestone %s deleted' % self.name,
+                                now, db=db)
+
+        if handle_ta:
+            db.commit()
+
+    def insert(self, db=None):
+        assert self.name, 'Cannot create milestone with no name'
+        self.name = self.name.strip()
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.debug("Creating new milestone '%s'" % self.name)
+        cursor.execute("INSERT INTO milestone (name,due,completed,description) "
+                       "VALUES (%s,%s,%s,%s)",
+                       (self.name, self.due, self.completed, self.description))
+
+        if handle_ta:
+            db.commit()
+
+    def update(self, db=None):
+        assert self.name, 'Cannot update milestone with no name'
+        self.name = self.name.strip()
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.info('Updating milestone "%s"' % self.name)
+        cursor.execute("UPDATE milestone SET name=%s,due=%s,"
+                       "completed=%s,description=%s WHERE name=%s",
+                       (self.name, self.due, self.completed, self.description,
+                        self._old_name))
+        self.env.log.info('Updating milestone field of all tickets '
+                          'associated with milestone "%s"' % self.name)
+        cursor.execute("UPDATE ticket SET milestone=%s WHERE milestone=%s",
+                       (self.name, self._old_name))
+        self._old_name = self.name
+
+        if handle_ta:
+            db.commit()
+
+    def select(cls, env, include_completed=True, db=None):
+        if not db:
+            db = env.get_db_cnx()
+        sql = "SELECT name,due,completed,description FROM milestone "
+        if not include_completed:
+            sql += "WHERE COALESCE(completed,0)=0 "
+        cursor = db.cursor()
+        cursor.execute(sql)
+        milestones = []
+        for name,due,completed,description in cursor:
+            milestone = Milestone(env)
+            milestone.name = milestone._old_name = name
+            milestone.due = due and int(due) or 0
+            milestone.completed = completed and int(completed) or 0
+            milestone.description = description or ''
+            milestones.append(milestone)
+        def milestone_order(m):
+            return (m.completed or sys.maxint,
+                    m.due or sys.maxint,
+                    embedded_numbers(m.name))
+        return sorted(milestones, key=milestone_order)
+    select = classmethod(select)
+
+
+class Version(object):
+
+    def __init__(self, env, name=None, db=None):
+        self.env = env
+        if name:
+            if not db:
+                db = self.env.get_db_cnx()
+            cursor = db.cursor()
+            cursor.execute("SELECT time,description FROM version "
+                           "WHERE name=%s", (name,))
+            row = cursor.fetchone()
+            if not row:
+                raise TracError, 'Version %s does not exist.' % name
+            self.name = self._old_name = name
+            self.time = row[0] and int(row[0]) or None
+            self.description = row[1] or ''
+        else:
+            self.name = self._old_name = None
+            self.time = None
+            self.description = None
+
+    exists = property(fget=lambda self: self._old_name is not None)
+
+    def delete(self, db=None):
+        assert self.exists, 'Cannot deleting non-existent version'
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.info('Deleting version %s' % self.name)
+        cursor.execute("DELETE FROM version WHERE name=%s", (self.name,))
+
+        self.name = self._old_name = None
+
+        if handle_ta:
+            db.commit()
+
+    def insert(self, db=None):
+        assert not self.exists, 'Cannot insert existing version'
+        assert self.name, 'Cannot create version with no name'
+        self.name = self.name.strip()
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.debug("Creating new version '%s'" % self.name)
+        cursor.execute("INSERT INTO version (name,time,description) "
+                       "VALUES (%s,%s,%s)",
+                       (self.name, self.time, self.description))
+
+        if handle_ta:
+            db.commit()
+
+    def update(self, db=None):
+        assert self.exists, 'Cannot update non-existent version'
+        assert self.name, 'Cannot update version with no name'
+        self.name = self.name.strip()
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.info('Updating version "%s"' % self.name)
+        cursor.execute("UPDATE version SET name=%s,time=%s,description=%s "
+                       "WHERE name=%s",
+                       (self.name, self.time, self.description,
+                        self._old_name))
+        if self.name != self._old_name:
+            # Update tickets
+            cursor.execute("UPDATE ticket SET version=%s WHERE version=%s",
+                           (self.name, self._old_name))
+            self._old_name = self.name
+
+        if handle_ta:
+            db.commit()
+
+    def select(cls, env, db=None):
+        if not db:
+            db = env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT name,time,description FROM version")
+        versions = []
+        for name, time, description in cursor:
+            version = cls(env)
+            version.name = name
+            version.time = time and int(time) or None
+            version.description = description or ''
+            versions.append(version)
+        def version_order(v):
+            return (v.time or sys.maxint, embedded_numbers(v.name))
+        return sorted(versions, key=version_order, reverse=True)
+    select = classmethod(select)
Copyright (C) 2012-2017 Edgewall Software