Index: trac/attachment.py
===================================================================
--- trac/attachment.py	(revision 7178)
+++ trac/attachment.py	(working copy)
@@ -451,16 +451,24 @@
             time = datetime.fromtimestamp(ts, utc)
             yield ('created', realm, id, filename, time, description, author)
 
-    def get_timeline_events(self, req, resource_realm, start, stop):
+    def get_timeline_events(self, req, resource_realm, start, stop, filters):
         """Return an event generator suitable for ITimelineEventProvider.
+        This is called from the parent resource, it doesn't directly implement
+        ITimelineEventProvider.
 
         Events are changes to attachments on resources of the given
         `resource_realm.realm`.
         """
+        if filters.get('author', ''):
+            filter_author = lambda author: author == filters['author']
+        else:
+            filter_author = lambda author: True
         for change, realm, id, filename, time, descr, author in \
                 self.get_history(start, stop, resource_realm.realm):
             attachment = resource_realm(id=id).child('attachment', filename)
             if 'ATTACHMENT_VIEW' in req.perm(attachment):
+                if not filter_author(author):
+                    continue
                 yield ('attachment', time, author, (attachment, descr), self)
 
     def render_timeline_event(self, context, field, event):
Index: trac/ticket/web_ui.py
===================================================================
--- trac/ticket/web_ui.py	(revision 7178)
+++ trac/ticket/web_ui.py	(working copy)
@@ -199,9 +199,10 @@
 
     def get_timeline_filters(self, req):
         if 'TICKET_VIEW' in req.perm:
-            yield ('ticket', _('Ticket changes'))
+            yield ('ticket', ITimelineEventProvider.CHECKBOX, _('Ticket changes'), True)
+            yield ('author', ITimelineEventProvider.STRING, _('Author'), '')
             if self.timeline_details:
-                yield ('ticket_details', _('Ticket details'), False)
+                yield ('ticket_details', ITimelineEventProvider.CHECKBOX, _('Ticket details'), False)
 
     def get_timeline_events(self, req, start, stop, filters):
         ts_start = to_timestamp(start)
@@ -213,6 +214,10 @@
                       'edit': ('editedticket', 'updated')}
 
         ticket_realm = Resource('ticket')
+        if filters.get('author', ''):
+            filter_author = lambda author: author == filters['author']
+        else:
+            filter_author = lambda author: True
 
         def produce_event((id, ts, author, type, summary, description),
                           status, fields, comment, cid):
@@ -221,6 +226,8 @@
                 return None
             resolution = fields.get('resolution')
             info = ''
+            if not filter_author(author):
+                return None
             if status == 'edit':
                 if 'ticket_details' in filters:
                     if len(fields) > 0:
@@ -290,7 +297,7 @@
             # Attachments
             if 'ticket_details' in filters:
                 for event in AttachmentModule(self.env).get_timeline_events(
-                    req, ticket_realm, start, stop):
+                    req, ticket_realm, start, stop, filters):
                     yield event
 
     def render_timeline_event(self, context, field, event):
Index: trac/ticket/roadmap.py
===================================================================
--- trac/ticket/roadmap.py	(revision 7178)
+++ trac/ticket/roadmap.py	(working copy)
@@ -501,7 +501,7 @@
 
     def get_timeline_filters(self, req):
         if 'MILESTONE_VIEW' in req.perm:
-            yield ('milestone', _('Milestones'))
+            yield ('milestone', ITimelineEventProvider.CHECKBOX, _('Milestones'), True)
 
     def get_timeline_events(self, req, start, stop, filters):
         if 'milestone' in filters:
@@ -520,7 +520,7 @@
 
             # Attachments
             for event in AttachmentModule(self.env).get_timeline_events(
-                req, milestone_realm, start, stop):
+                req, milestone_realm, start, stop, filters):
                 yield event
                 
     def render_timeline_event(self, context, field, event):
Index: trac/versioncontrol/web_ui/changeset.py
===================================================================
--- trac/versioncontrol/web_ui/changeset.py	(revision 7178)
+++ trac/versioncontrol/web_ui/changeset.py	(working copy)
@@ -764,9 +764,14 @@
 
     def get_timeline_filters(self, req):
         if 'CHANGESET_VIEW' in req.perm:
-            yield ('changeset', _('Repository checkins'))
+            yield ('changeset', ITimelineEventProvider.CHECKBOX, _('Repository checkins'), True)
+            yield ('author', ITimelineEventProvider.STRING, _('Author'), '')
 
     def get_timeline_events(self, req, start, stop, filters):
+        if filters.get('author', ''):
+            filter_author = lambda author: author == filters['author']
+        else:
+            filter_author = lambda author: True
         if 'changeset' in filters:
             show_files = self.timeline_show_files
             show_location = show_files == 'location'
@@ -792,6 +797,8 @@
                         permitted_changesets.append(chgset)
                 if permitted_changesets:
                     chgset = permitted_changesets[-1]
+                    if not filter_author(chgset.author):
+                        continue
                     yield ('changeset', chgset.date, chgset.author,
                            (permitted_changesets, chgset.message or '',
                             show_location, show_files))
Index: trac/admin/console.py
===================================================================
--- trac/admin/console.py	(revision 7178)
+++ trac/admin/console.py	(working copy)
@@ -107,6 +107,7 @@
         except TracError, e:
             print>>sys.stderr, 'Command failed: %s' % e
             rv = 2
+            raise
         if not self.interactive:
             return rv
 
Index: trac/wiki/web_ui.py
===================================================================
--- trac/wiki/web_ui.py	(revision 7178)
+++ trac/wiki/web_ui.py	(working copy)
@@ -549,10 +549,15 @@
 
     def get_timeline_filters(self, req):
         if 'WIKI_VIEW' in req.perm:
-            yield ('wiki', _('Wiki changes'))
+            yield ('wiki', ITimelineEventProvider.CHECKBOX, _('Wiki changes'), True)
+            yield ('author', ITimelineEventProvider.STRING, _('Author'), '')
 
     def get_timeline_events(self, req, start, stop, filters):
         db = self.env.get_db_cnx()
+        if filters.get('author', ''):
+            filter_author = lambda author: author == filters['author']
+        else:
+            filter_author = lambda author: True
         if 'wiki' in filters:
             wiki_realm = Resource('wiki')
             cursor = db.cursor()
@@ -563,12 +568,14 @@
                 wiki_page = wiki_realm(id=name, version=version)
                 if 'WIKI_VIEW' not in req.perm(wiki_page):
                     continue
+                if not filter_author(author):
+                    continue
                 yield ('wiki', datetime.fromtimestamp(ts, utc), author,
                        (wiki_page, comment))
 
             # Attachments
             for event in AttachmentModule(self.env).get_timeline_events(
-                req, wiki_realm, start, stop):
+                req, wiki_realm, start, stop, filters):
                 yield event
 
     def render_timeline_event(self, context, field, event):
Index: trac/timeline/api.py
===================================================================
--- trac/timeline/api.py	(revision 7178)
+++ trac/timeline/api.py	(working copy)
@@ -30,23 +30,27 @@
     timeline.
     """
 
+    CHECKBOX = 'checkbox'
+    STRING = 'string'
+
     def get_timeline_filters(req):
         """Return a list of filters that this event provider supports.
         
-        Each filter must be a (name, label) tuple, where `name` is the internal
-        name, and `label` is a human-readable name for display.
-
-        Optionally, the tuple can contain a third element, `checked`.
-        If `checked` is omitted or True, the filter is active by default,
-        otherwise it will be inactive.
+        Each filter must be a (name, type, label, default) tuple, where
+        `name` is the internal name,
+        `label` is a human-readable name for display,
+        `type` is either CHECKBOX or STRING
+        `default` is the default option for this filter
         """
 
     def get_timeline_events(req, start, stop, filters):
         """Return a list of events in the time range given by the `start` and
         `stop` parameters.
 
-        The `filters` parameters is a list of the enabled filters, each item
-        being the name of the tuples returned by `get_timeline_filters`.
+        The `filters` parameters is a dictionary of the enabled filters, each
+        key being the name of the tuples returned by `get_timeline_filters`,
+        and each value being either `True` for a `CHECKBOX` filter or the string
+        passed to a `STRING` filter. `False` `CHECKBOX` filters are not included.
 
         Since 0.11, the events are `(kind, date, author, data)` tuples,
         where `kind` is a string used for categorizing the event, `date`
Index: trac/timeline/web_ui.py
===================================================================
--- trac/timeline/web_ui.py	(revision 7178)
+++ trac/timeline/web_ui.py	(working copy)
@@ -128,24 +128,46 @@
 
         available_filters = []
         for event_provider in self.event_providers:
-            available_filters += event_provider.get_timeline_filters(req)
+            for timeline_filter in event_provider.get_timeline_filters(req):
+                # support for previous timeline filter format (Deprecated)
+                if len(timeline_filter) == 2:
+                    timeline_filter = (timeline_filter[0], ITimelineEventProvider.CHECKBOX,
+                                       timeline_filter[1], True)
+                elif len(timeline_filter) == 3:
+                    timeline_filter = (timeline_filter[0], ITimelineEventProvider.CHECKBOX,
+                                       timeline_filter[1], timeline_filter[2])
+                available_filters.append(timeline_filter)
 
-        filters = []
+        filters = {}
         # check the request or session for enabled filters, or use default
-        for test in (lambda f: f[0] in req.args,
-                     lambda f: req.session.get('timeline.filter.%s' % f[0],
-                                               '') == '1',
-                     lambda f: len(f) == 2 or f[2]):
-            if filters:
-                break
-            filters = [f[0] for f in available_filters if test(f)]
+        for f in available_filters:
+            filter_name = f[0]
+            if filter_name in filters:
+                continue
+            if f[1] == ITimelineEventProvider.CHECKBOX:
+                if filter_name in req.args:
+                    filters[filter_name] = True
+                elif req.session.get('timeline.filter.%s' % filter_name, '') == '1':
+                    filters[filter_name] = True
+                elif f[3]:
+                    filters[filter_name] = True
+            elif f[1] == ITimelineEventProvider.STRING:
+                if filter_name in req.args:
+                    filters[filter_name] = req.args[filter_name]
+                elif req.session.get('timeline.filter.%s' % filter_name, None) is not None:
+                    filters[filter_name] = req.session.get('timeline.filter.%s' % filter_name)
+                else:
+                    filters[filter_name] = f[3]
 
         # save the results of submitting the timeline form to the session
         if 'update' in req.args:
             for filter in available_filters:
                 key = 'timeline.filter.%s' % filter[0]
                 if filter[0] in req.args:
-                    req.session[key] = '1'
+                    if filter[1] == ITimelineEventProvider.CHECKBOX:
+                        req.session[key] = '1'
+                    elif filter[1] == ITimelineEventProvider.STRING:
+                        req.session[key] = req.args[filter[0]]
                 elif key in req.session:
                     del req.session[key]
 
@@ -190,10 +212,20 @@
         add_link(req, 'alternate', rss_href, _('RSS Feed'),
                  'application/rss+xml', 'rss')
 
+        filter_options_check = {}
         for filter_ in available_filters:
-            data['filters'].append({'name': filter_[0], 'label': filter_[1],
-                                    'enabled': filter_[0] in filters})
-
+            if filter_[0] in filter_options_check:
+                continue
+            if filter_[1] == ITimelineEventProvider.CHECKBOX:
+                filter_options_check[filter_[0]] = True
+                data['filters'].append({'name': filter_[0], 'filter_type': filter_[1],
+                                        'label': filter_[2],
+                                        'enabled': filters.get(filter_[0], False)})
+            elif filter_[1] == ITimelineEventProvider.STRING:
+                filter_options_check[filter_[0]] = True
+                data['filters'].append({'name': filter_[0], 'filter_type': filter_[1],
+                                        'label': filter_[2], 'enabled': True,
+                                        'value': filters.get(filter_[0], filter_[3])})
         # Navigation to the previous/next period of 'daysback' days
         previous_start = format_date(fromdate - timedelta(days=daysback+1),
                                      format='%Y-%m-%d', tzinfo=req.tz)
@@ -305,7 +337,7 @@
         self.log.exception('Timeline event provider %s failed', ep_name)
 
         guilty_filters = [f[0] for f in ep.get_timeline_filters(req)]
-        guilty_kinds = [f[1] for f in ep.get_timeline_filters(req)]
+        guilty_labels = [f[2] for f in ep.get_timeline_filters(req)]
         other_filters = [f for f in current_filters if not f in guilty_filters]
         if not other_filters:
             other_filters = [f for f in all_filters if not f in guilty_filters]
@@ -313,7 +345,7 @@
                                                'daysback')]
         href = req.href.timeline(args+[(f, 'on') for f in other_filters])
         raise TracError(tag(
-            tag.p(', '.join(guilty_kinds),
+            tag.p(', '.join(guilty_labels),
                   ' event provider (', tag.tt(ep_name), ') failed:', tag.br(),
                   exc_name, ': ', to_unicode(exc), class_='message'),
             tag.p('You may want to see the other kind of events from the ',
Index: trac/timeline/templates/timeline.html
===================================================================
--- trac/timeline/templates/timeline.html	(revision 7178)
+++ trac/timeline/templates/timeline.html	(working copy)
@@ -21,10 +21,14 @@
         and <label><input type="text" size="3" name="daysback" value="$daysback" /> days back</label>.
        </div>
        <fieldset>
-        <label py:for="filter in filters">
+        <label py:for="filter in filters" py:if="filter.filter_type == 'checkbox'">
           <input type="checkbox" name="${filter.name}"
                  checked="${filter.enabled or None}"/> ${filter.label}
         </label>
+        <label py:for="filter in filters" py:if="filter.filter_type == 'string'">
+          ${filter.label} 
+          <input type="text" name="${filter.name}" value="${filter.value}"/>
+        </label>
        </fieldset>
        <div class="buttons">
         <input type="submit" name="update" value="Update" />
Index: contrib/workflow/graph-workflows
===================================================================
--- contrib/workflow/graph-workflows	(revision 0)
+++ contrib/workflow/graph-workflows	(revision 0)
@@ -0,0 +1,9 @@
+#!/bin/bash
+for f in *.ini ../../trac/ticket/workflows/*.ini
+  do
+    echo $f
+    dot=`basename "$f" .ini`.dot
+    python workflow_parser.py $f | grep -v rotate > $dot
+    dot $dot -Tpng -O
+  done
+

Property changes on: contrib/workflow/graph-workflows
___________________________________________________________________
Name: svn:executable
   + *


