> Not at this time.  Alec deleted the old branch since major changes in
> the trunk made it more difficult to merge those changes into the branch
> than to start fresh and reimplement the changes on top of the current
> trunk.  Most of the changes have not yet been added back to the new
> branch, so it's not very useful at this time.  You can go back and
> check out the earlier branch prior to its deletion, but of course the
> APIs are likely to change before it's completed.

This news is bit disheartening.

We are currently heavily using Trac at our shop and have found the
workflow branch enormously helpful in allowing us to create plugins for
milestone and ticket dependencies and ticket priorities. Are there any
estimates on the time and work required to reintegrate the workflow
changes? Workflow api enhancements are something I can see my company
devoting resources in helping your project. Please advise.

I can be contacted directly at: kkurzweil (at) lulu (dot) com

I have also included a diff of a few of the suttle changes we have made
so far. They, and ofcourse the changes found in the workflow branch,
have allowed us to write the plugins we needed for our process. These
were some of the changes we would have liked to eventually submit as
patches.

Index: model.py
===================================================================
--- model.py    (.../vendor/workflow-r3578/trac/ticket/model.py)        
(revision
564)
+++
model.py        (.../tags/20061121_pre_time_tracking/trac/ticket/model.py)      
(revision
564)
@@ -22,7 +22,7 @@
 import re

 from trac.core import TracError
-from trac.ticket import TicketSystem
+from trac.ticket import TicketSystem, MilestoneSystem
 from trac.util import sorted, embedded_numbers

 __all__ = ['Ticket', 'Type', 'Status', 'Resolution', 'Priority',
'Severity',
@@ -171,13 +171,16 @@
                                "VALUES (%s,%s,%s)", [(tkt_id, name,
self[name])
                                                      for name in
custom_fields])

+        # Moved to before ticket_created() calls to use the new ticket
id
+        # when setting dependent ticket parent/children 6/29/2006 NAK.
+        self.id = tkt_id
+        self._old = {}
+
         for listener in TicketSystem(self.env).change_listeners:
             listener.ticket_created(self)

         if handle_ta:
             db.commit()
-        self.id = tkt_id
-        self._old = {}
         return self.id

     def save_changes(self, author, comment, when=0, db=None, cnum=''):
@@ -569,6 +572,9 @@

     def __init__(self, env, name=None, db=None):
         self.env = env
+        self.fields = MilestoneSystem(self.env).get_milestone_fields()
+        self.values = {}
+        self._init_defaults()
         if name:
             self._fetch(name, db)
             self._old_name = name
@@ -576,10 +582,35 @@
             self.name = self._old_name = None
             self.due = self.completed = 0
             self.description = ''
+        self._old = {}

+    def _init_defaults(self):
+        for field in self.fields:
+            default = None
+            if not field.custom:
+                # We only support custom fields at this time.
+                pass
+            else:
+                default = field.value
+                options = getattr(field, '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(self, name, db=None):
         if not db:
             db = self.env.get_db_cnx()
+
+        # Fetch the standard milestone fields
         cursor = db.cursor()
         cursor.execute("SELECT name,due,completed,description "
                        "FROM milestone WHERE name=%s", (name,))
@@ -592,11 +623,58 @@
         self.completed = row[2] and int(row[2]) or 0
         self.description = row[3] or ''

+        # Fetch custom fields if available
+        custom_fields = [f.name for f in self.fields if f.custom]
+        cursor.execute("SELECT name,value FROM milestone_custom "
+                       "WHERE milestone=%s", (name,))
+        for fname, value in cursor:
+            if fname in custom_fields:
+                self.values[fname] = value
+
+    def __getitem__(self, name):
+        if self.__dict__.has_key(name):
+            return self.__dict__[name]
+        elif self.values.has_key(name):
+            return self.values[name]
+        else:
+            #return ''
+            raise KeyError("Invalid Milestone (%s) field: %s" \
+                           % (self.name, name))
+
+    def __setitem__(self, name, value):
+        if self.__dict__.has_key(name):
+            self.__dict__[name] = value
+        else:
+            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:
+                fields = [field for field in self.fields if field.name
== name]
+                if fields and fields[0].type != 'textarea':
+                    value = value.strip()
+                    value = value.strip('\'"')
+            self.values[name] = value
+
     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 populate(self, values):
+        """Populate the milestone 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 delete(self, retarget_to=None, author=None, db=None):
         if not db:
             db = self.env.get_db_cnx()
@@ -607,6 +685,8 @@
         cursor = db.cursor()
         self.env.log.info('Deleting milestone %s' % self.name)
         cursor.execute("DELETE FROM milestone WHERE name=%s",
(self.name,))
+        cursor.execute("DELETE FROM milestone_custom WHERE
milestone=%s",
+                       (self.name,))

         # Retarget/reset tickets associated with this milestone
         now = time.time()
@@ -639,6 +719,16 @@
                        "VALUES (%s,%s,%s,%s)",
                        (self.name, self.due, self.completed,
self.description))

+        # Insert custom fields
+        custom_fields = [f.name for f in self.fields if f.custom
+                         and self.values.has_key(f.name)]
+        if custom_fields:
+            cursor.executemany("INSERT INTO milestone_custom "
+                               "(milestone,name,value) VALUES
(%s,%s,%s)",
+                               [(self.name, name, self[name])
+                                    for name in custom_fields])
+        self._old = {}
+
         for listener in
TicketSystem(self.env).adjunct_change_listeners:
             listener.adjunct_created('milestone', self)

@@ -656,37 +746,70 @@

         cursor = db.cursor()
         self.env.log.info('Updating milestone "%s"' % self.name)
+
+        custom_fields = [f.name for f in self.fields if f.custom]
+        for name in custom_fields:
+            if name in self._old.keys():
+                #self.env.log.debug("Updating field %s" % (name))
+                cursor.execute("SELECT * FROM milestone_custom "
+                               "WHERE milestone=%s and name=%s",
+                               (self._old_name, name))
+                if cursor.fetchone():
+                    cursor.execute("UPDATE milestone_custom SET
value=%s "
+                                   "WHERE milestone=%s AND name=%s",
+                                   (self[name], self._old_name, name))
+                else:
+                    cursor.execute("INSERT INTO milestone_custom "
+                                   "(milestone,name,value)
VALUES(%s,%s,%s)",
+                                   (self._old_name, name, 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 self._old_name != self.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

+        # Temporary assignments for use in the change listeners
8/3/2006 NAK.
+        self.old_name = self._old_name
+        self.old = self._old
+
         for listener in
TicketSystem(self.env).adjunct_change_listeners:
             listener.adjunct_changed('milestone', self)

         if handle_ta:
             db.commit()
+        self._old = {}
+        del self.old_name
+        del self.old

-    def select(cls, env, include_completed=True, db=None):
+    def select(cls, env, by_type='', include_completed=True, db=None):
         if not db:
             db = env.get_db_cnx()
-        sql = "SELECT name,due,completed,description FROM milestone "
+        sql = "SELECT m.name,m.due,m.completed,m.description,c.value "
\
+                + "FROM milestone m " \
+                + "LEFT OUTER JOIN milestone_custom c " \
+                + "ON (m.name = c.milestone AND c.name = 'type') "
+        if by_type and len(by_type) > 0:
+            sql += "WHERE COALESCE(value,'release')='%s' " % (by_type)
+        else:
+            sql += "WHERE 1=1 "
         if not include_completed:
-            sql += "WHERE COALESCE(completed,0)=0 "
+            sql += "AND COALESCE(completed,0)=0 "
         cursor = db.cursor()
         cursor.execute(sql)
         milestones = []
-        for name,due,completed,description in cursor:
+        for name,due,completed,description,type 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 ''
+            milestone.type = type
             milestones.append(milestone)
         def milestone_order(m):
             return (m.completed or sys.maxint,
Index: api.py
===================================================================
--- api.py      (.../vendor/workflow-r3578/trac/ticket/api.py)  (revision
564)
+++
api.py  (.../tags/20061121_pre_time_tracking/trac/ticket/api.py)        
(revision
564)
@@ -133,6 +133,46 @@
         list of tuples (score, id) where score is between 0.0 and
1.0."""


+class IMilestoneFieldProvider(Interface):
+    """ Provide custom milestone fields programmatically. Allows
plugins to
+    supply fields without manipulation of the [milestone-custom]
section of the
+    trac.ini file. """
+
+    def get_milestone_fields():
+        """ Return an iterable of custom trac.ticket.field.Field
objects. """
+
+    def validate_milestone_field(req, milestone, field, value):
+        """ Validate custom milestone field value after user
submission.
+        Returns a list of validation error messages. """
+
+
+class IMilestoneFieldTypeProvider(Interface):
+    """ Provide custom milestone field types (subclasses of
+    trac.ticket.field.Field). """
+
+    def get_milestone_field_types():
+        """ Return list of (name, type) tuples, where type is a
subclass of
+        trac.ticket.field.Field. """
+
+    def validate_milestone_field_type(req, milestone, field, value):
+        """ Validate a field of type provided by this extension.
Returns a list
+        of validation error messages. """
+
+
+class IMilestoneManipulator(Interface):
+    """ Miscellaneous manipulation of milestone workflow features. """
+    def prepare_milestone(req, milestone, fields):
+        """ Prepare the milestone fields for rendering. Fields can be
modified.
+        fields is a list of Field objects. """
+
+    def validate_milestone(req, milestone):
+        """ Validate milestone state after population from user
provided
+        values.  Must return a list of `(field, message)` tuples, one
for each
+        problem detected. `field` can be `None` to indicate an overall
problem
+        with the milestone. Therefore, a return value of `[]` means
everything
+        is OK. """
+
+
 class DefaultSimilarityDetector(Component):
     """ Default ticket similarity detector. Uses the number of common
words
     between ticket summary description and keywords, weighted by their
length.
@@ -330,7 +370,8 @@

 class BuiltinFieldProvider(Component):
     """ Provide builtin ticket fields and field types. """
-    implements(ITicketFieldProvider, ITicketFieldTypeProvider)
+    implements(ITicketFieldProvider, ITicketFieldTypeProvider, \
+               IMilestoneFieldProvider, IMilestoneFieldTypeProvider)

     # ITicketFieldProvider methods
     def _add_enum_field(self, db, fields, cls, **kwargs):
@@ -431,18 +472,38 @@
     def validate_ticket_field_type(self, req, ticket, field, value):
         return []

+
+    # IMilestoneFieldProvider methods
+    def get_milestone_fields(self):
+        return []
+
+    def validate_milestone_field(self, req, milestone, field, value):
+        return []
+
+
+    # IMilestoneFieldTypeProvider methods
+
+    def get_milestone_field_types(self):
+        return self.get_ticket_field_types()
+
+    def validate_milestone_field_type(self, req, milestone, field,
value):
+        return []
+
+
 class CustomFieldProvider(Component):
-    """ Generate custom ticket fields from the `ticket-custom` section
of
-        TracIni. Details in TracTicketsCustomFields. """
-    implements(ITicketFieldProvider)
+    """ Generate custom ticket fields from the `ticket-custom` and
+    `milestone-custom` sections of TracIni. Details in
TracTicketsCustomFields.
+    """
+    implements(ITicketFieldProvider, IMilestoneFieldProvider)

-    field_type_providers = ExtensionPoint(ITicketFieldTypeProvider)
+    field_type_providers = ticket_field_type_providers = \
+        ExtensionPoint(ITicketFieldTypeProvider)
+    milestone_field_type_providers =
ExtensionPoint(IMilestoneFieldTypeProvider)

-    # ITicketFieldProvider methods
-    def get_ticket_fields(self):
+    # Utility methods
+
+    def _get_custom_fields(self, config, field_types):
         fields = []
-        config = self.config['ticket-custom']
-        field_types = TicketSystem(self.env).get_ticket_field_types()
         for name in [option for option, value in config.options()
                      if '.' not in option]:
             if not re.match('^[a-zA-Z][a-zA-Z0-9_]+$', name):
@@ -463,10 +524,29 @@

         return fields

+
+    # ITicketFieldProvider methods
+
+    def get_ticket_fields(self):
+        config = self.config['ticket-custom']
+        field_types = TicketSystem(self.env).get_ticket_field_types()
+        return self._get_custom_fields(config, field_types)
+
     def validate_ticket_field(self, req, ticket, field, value):
         return []


+    # IMilestoneFieldProvider methods
+
+    def get_milestone_fields(self):
+        config = self.config['milestone-custom']
+        field_types =
MilestoneSystem(self.env).get_milestone_field_types()
+        return self._get_custom_fields(config, field_types)
+
+    def validate_milestone_field(self, req, milestone, field, value):
+        return []
+
+
 class TicketSystem(Component):
     implements(IPermissionRequestor, IWikiSyntaxProvider,
ISearchSource)

@@ -634,6 +714,37 @@
                    date, author, shorten_result(desc, terms))


+class MilestoneSystem(Component):
+    """ Added to support custom milestone fields 8/1/2006 NAK.
+        This differs from custom ticket fields in one important
respect: you
+        can only add _custom_ fields with this interface.  The
standard
+        built-in milestone fields cannot be changed.  """
+
+    field_providers = ExtensionPoint(IMilestoneFieldProvider)
+    field_type_providers = ExtensionPoint(IMilestoneFieldTypeProvider)
+
+    # Public methods
+    def get_milestone_fields(self):
+        """ Get all milestone fields. """
+        fields = []
+        for field_provider in self.field_providers:
+            for field in field_provider.get_milestone_fields():
+                field.field_provider = field_provider
+                fields.append(field)
+        fields.sort(lambda x, y: cmp(x.order, y.order))
+        return fields
+
+    def get_milestone_field_types(self):
+        """ Get supported field types as a dictionary with type name
as the key
+        and Field subclass as the value. """
+        field_types = {}
+        for type_provider in self.field_type_providers:
+            for name, field_type in
type_provider.get_milestone_field_types():
+                field_type.field_type_provider = type_provider
+                field_types[name] = field_type
+        return field_types
+
+
 class Similarity:
     """ Determine similarity between two blocks of text. """
     def __init__(self, text, min_wordsize=3):


--~--~---------~--~----~------------~-------~--~----~
 You received this message because you are subscribed to the Google Groups 
"Trac Development" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/trac-dev?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to