changeset:   31:f08a0d1b8960
tag:         qtip
tag:         overhaul
tag:         tip
tag:         qbase
user:        Christian Boos <cboos@neuf.fr>
date:        Mon Oct 22 12:34:10 2007 +0200
files:       trac/attachment.py trac/ticket/roadmap.py trac/ticket/web_ui.py trac/timeline/api.py trac/timeline/templates/timeline.html trac/timeline/templates/timeline.rss trac/timeline/web_ui.py trac/versioncontrol/web_ui/changeset.py trac/wiki/formatter.py trac/wiki/web_ui.py
description:
Timeline refactoring: clean-up the API changes that were introduced during 0.11dev.

The main idea of the original refactoring has been kept: don't render the events directly when generating them, but defer this last step when the event is actually processed within the template.

But instead of requiring a intermediate `TimelineEvent` object, we now rely on a `render_timeline_event` method of the `ITimelineEventProvider` to do the job. The event itself is still a tuple, but of different arity than the one of 0.10, so that we can easily make the difference and keep backward compatibility. The new tuple enable the components to add an arbitrary amount of "private" data to the tuple, for the rendering needs


diff -r 00135419c47f -r f08a0d1b8960 trac/attachment.py
--- a/trac/attachment.py	Mon Oct 22 12:25:11 2007 +0200
+++ b/trac/attachment.py	Mon Oct 22 12:34:10 2007 +0200
@@ -32,7 +32,6 @@ from trac.env import IEnvironmentSetupPa
 from trac.env import IEnvironmentSetupParticipant
 from trac.perm import PermissionError, PermissionSystem, IPermissionPolicy
 from trac.mimeview import *
-from trac.timeline.api import TimelineEvent
 from trac.util import get_reporter_id, create_unique_file, content_disposition
 from trac.util.datefmt import to_timestamp, utc
 from trac.util.text import unicode_quote, unicode_unquote, pretty_size
@@ -41,6 +40,7 @@ from trac.web.chrome import add_link, ad
 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
 from trac.web.href import Href
 from trac.wiki.api import IWikiSyntaxProvider
+from trac.wiki.formatter import format_to_oneliner
 
 
 class InvalidAttachment(TracError):
@@ -431,22 +431,21 @@ class AttachmentModule(Component):
         """
         for change, realm, id, filename, time, descr, author in \
                 self.get_history(start, stop, resource_realm.realm):
-            parent = resource_realm(id=id)
-            attachment = parent.child('attachment', filename)
+            attachment = resource_realm(id=id).child('attachment', filename)
             if 'ATTACHMENT_VIEW' in req.perm(attachment):
-                title = tag(tag.em(os.path.basename(filename)),
-                            _(" attached to "),
-                            tag.em(get_name(self.env, parent),
-                                   title=get_summary(self.env, parent)))
-                ### FIXME: the link is no longer to the attachment...
-                event = TimelineEvent(self, 'attachment')
-                event.set_changeinfo(time, author)
-                event.add_markup(title=title)
-                event.add_wiki(parent, body=descr)
-                yield event
-
-    def event_formatter(self, event, key):
-        return None
+                yield ('attachment', time, author, (attachment, descr), self)
+
+    def render_timeline_event(self, context, field, event):
+        attachment, descr = event[3]
+        if field == 'url':
+            return self.get_resource_url(attachment, context.href)
+        elif field == 'title':
+            return tag(tag.em(os.path.basename(attachment.id)),
+                       _(" attached to "),
+                       tag.em(get_name(self.env, attachment.parent),
+                              title=get_summary(self.env, attachment.parent)))
+        elif field == 'description':
+            return format_to_oneliner(context(attachment.parent), descr)
     
     # IResourceManager methods
     
diff -r 00135419c47f -r f08a0d1b8960 trac/ticket/roadmap.py
--- a/trac/ticket/roadmap.py	Mon Oct 22 12:25:11 2007 +0200
+++ b/trac/ticket/roadmap.py	Mon Oct 22 12:34:10 2007 +0200
@@ -23,6 +23,7 @@ from genshi.builder import tag
 
 from trac import __version__
 from trac.attachment import AttachmentModule
+from trac.config import ExtensionOption
 from trac.context import *
 from trac.core import *
 from trac.perm import IPermissionRequestor
@@ -34,11 +35,11 @@ from trac.util.translation import _
 from trac.util.translation import _
 from trac.ticket import Milestone, Ticket, TicketSystem
 from trac.ticket.query import Query
-from trac.timeline.api import ITimelineEventProvider, TimelineEvent
+from trac.timeline.api import ITimelineEventProvider
 from trac.web import IRequestHandler
 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
 from trac.wiki.api import IWikiSyntaxProvider
-from trac.config import ExtensionOption
+from trac.wiki.formatter import format_to_html
 
 class ITicketGroupStatsProvider(Interface):
     def get_ticket_group_stats(self, ticket_ids):
@@ -506,25 +507,26 @@ class MilestoneModule(Component):
             cursor.execute("SELECT completed,name,description FROM milestone "
                            "WHERE completed>=%s AND completed<=%s",
                            (to_timestamp(start), to_timestamp(stop)))
-            for ts, name, description in cursor:
+            for completed, name, description in cursor:
                 milestone = milestone_realm(id=name)
                 if 'MILESTONE_VIEW' in req.perm(milestone):
-                    completed = datetime.fromtimestamp(ts, utc)
-                    title = tag('Milestone ', tag.em(name), ' completed')
-                    event = TimelineEvent('milestone', title,
-                                          req.href.milestone(name))
-                    event.set_changeinfo(completed, '') # FIXME: store the author
-                    event.add_wiki(milestone, description) # FIXME xxxo
-                    yield event
+                    yield('milestone', datetime.fromtimestamp(completed, utc),
+                          '', (milestone, description)) # FIXME: author?
 
             # Attachments
             for event in AttachmentModule(self.env).get_timeline_events(
                 req, milestone_realm, start, stop):
                 yield event
                 
-
-    def event_formatter(self, event, key):
-        return None
+    def render_timeline_event(self, context, field, event):
+        milestone, description = event[3]
+        if field == 'url':
+            return context.href.milestone(milestone.id)
+        elif field == 'title':
+            return tag('Milestone ', tag.em(milestone.id), ' completed')
+        elif field == 'description':
+            return format_to_html(context(resource=milestone),
+                                  shorten_line(description))
 
     # IRequestHandler methods
 
diff -r 00135419c47f -r f08a0d1b8960 trac/ticket/web_ui.py
--- a/trac/ticket/web_ui.py	Mon Oct 22 12:25:11 2007 +0200
+++ b/trac/ticket/web_ui.py	Mon Oct 22 12:34:10 2007 +0200
@@ -33,7 +33,7 @@ from trac.ticket import Milestone, Ticke
 from trac.ticket import Milestone, Ticket, TicketSystem, ITicketManipulator
 from trac.ticket import ITicketActionController
 from trac.ticket.notification import TicketNotifyEmail
-from trac.timeline.api import ITimelineEventProvider, TimelineEvent
+from trac.timeline.api import ITimelineEventProvider
 from trac.util import get_reporter_id
 from trac.util.compat import any
 from trac.util.datefmt import to_timestamp, utc
@@ -44,7 +44,7 @@ from trac.web import IRequestHandler
 from trac.web import IRequestHandler
 from trac.web.chrome import add_link, add_script, add_stylesheet, Chrome, \
                             INavigationContributor, ITemplateProvider
-
+from trac.wiki.formatter import format_to
 
 class InvalidTicket(TracError):
     """Exception raised when a ticket fails validation."""
@@ -204,17 +204,16 @@ class TicketModule(Component):
                       'reopened': ('reopenedticket', 'reopened'),
                       'closed': ('closedticket', 'closed'),
                       'edit': ('editedticket', 'updated')}
+
         ticket_realm = Resource('ticket')
-        ticketsystem = TicketSystem(self.env)
-        description = {}
-
-        def produce((id, ts, author, type, summary, description),
-                    status, fields, comment, cid):
-            resource = ticket_realm(id=id)
-            if 'TICKET_VIEW' not in req.perm(resource):
+
+        def produce_event((id, ts, author, type, summary, description),
+                          status, fields, comment, cid):
+            ticket = ticket_realm(id=id)
+            if 'TICKET_VIEW' not in req.perm(ticket):
                 return None
+            resolution = fields.get('resolution')
             info = ''
-            resolution = fields.get('resolution')
             if status == 'edit':
                 if 'ticket_details' in filters:
                     if len(fields) > 0:
@@ -231,25 +230,9 @@ class TicketModule(Component):
             else:
                 return None
             kind, verb = status_map[status]
-            title = ticketsystem.format_summary(summary, status,
-                                                resolution, type)
-            title = tag('Ticket ', tag.em(get_shortname(self.env, resource),
-                                          title=title),
-                        ' (', shorten_line(summary), ') ', verb)
-            markup = message = None
-            if status == 'new':
-                message = description
-            else:
-                markup = info
-                message = comment
-            t = datetime.fromtimestamp(ts, utc)
-            event = TimelineEvent(self, kind)
-            event.set_changeinfo(t, author)
-            event.add_markup(title=title, header=markup) # FIXME
-            event.add_wiki(resource, body=message) # FIXME
-            if cid:
-                event.href_fragment = '#comment:' + cid
-            return event
+            return (kind, datetime.fromtimestamp(ts, utc), author,
+                    (ticket, verb, info, summary, status, resolution, type,
+                     description, comment, cid))
 
         # Ticket changes
         db = self.env.get_db_cnx()
@@ -267,8 +250,8 @@ class TicketModule(Component):
             for id,t,author,type,summary,field,oldvalue,newvalue in cursor:
                 if not previous_update or (id,t,author) != previous_update[:3]:
                     if previous_update:
-                        ev = produce(previous_update, status, fields,
-                                     comment, cid)
+                        ev = produce_event(previous_update, status, fields,
+                                           comment, cid)
                         if ev:
                             yield ev
                     status, fields, comment, cid = 'edit', {}, '', None
@@ -281,7 +264,8 @@ class TicketModule(Component):
                 else:
                     fields[field] = newvalue
             if previous_update:
-                ev = produce(previous_update, status, fields, comment, cid)
+                ev = produce_event(previous_update, status, fields,
+                                   comment, cid)
                 if ev:
                     yield ev
 
@@ -292,7 +276,7 @@ class TicketModule(Component):
                                "  FROM ticket WHERE time>=%s AND time<=%s",
                                (ts_start, ts_stop))
                 for row in cursor:
-                    ev = produce(row, 'new', {}, None, None)
+                    ev = produce_event(row, 'new', {}, None, None)
                     if ev:
                         yield ev
 
@@ -302,11 +286,33 @@ class TicketModule(Component):
                     req, ticket_realm, start, stop):
                     yield event
 
-    def event_formatter(self, event, key):
-        flavor = 'oneliner'
-        if event.kind == 'newticket':
-            flavor = self.timeline_newticket_formatter
-        return (flavor, {})
+    def render_timeline_event(self, context, field, event):
+        ticket, verb, info, summary, status, resolution, type, \
+                description, comment, cid = event[3]
+        if field == 'url':
+            href = context.href.ticket(ticket.id)
+            if cid:
+                href += '#comment:' + cid
+            return href
+        elif field == 'title':
+            title = TicketSystem(self.env).format_summary(summary, status,
+                                                          resolution, type)
+            return tag('Ticket ', tag.em('#', ticket.id, title=title),
+                       ' (', shorten_line(summary), ') ', verb)
+        elif field == 'description':
+            descr = message = ''
+            if status == 'new':
+                message = description
+                flavor = self.timeline_newticket_formatter
+            else:
+                descr = info
+                message = comment
+                flavor = 'oneliner'
+            if message:
+                if self.config['timeline'].getbool('abbreviated_messages'):
+                    message = shorten_line(message)
+                descr += format_to(flavor, context(resource=ticket), message)
+            return descr
 
     # Internal methods
 
diff -r 00135419c47f -r f08a0d1b8960 trac/timeline/api.py
--- a/trac/timeline/api.py	Mon Oct 22 12:25:11 2007 +0200
+++ b/trac/timeline/api.py	Mon Oct 22 12:34:10 2007 +0200
@@ -25,119 +25,6 @@ from trac.web.href import Href
 from trac.web.href import Href
 
 
-class TimelineEvent(object):
-    """Group event related information.
-
-    WARNING: this interface is going to be overhauled
-
-    The first two properties are set in the constructor:
-
-    provider: reference to the event provider
-
-    kind: category of the event, will also be used as the CSS class for
-          the event's entry in the timeline
-
-    The following is set using the `add_markup` method.
-    
-    markup: dictionary of litteral informations regarding the events.
-            Standard keys include:
-             - 'title': short summary for the event
-             - 'header': markup that comes before the main body
-             - 'footer': markup that comes after the main body
-
-    The next two are set using the `add_wiki` method.
-    
-    resource: resource context
-    wikitext: dictionary of contextual information
-              Standard keys include:
-              `body` will be interpreted as the main text
-              `summary`
-
-    The next four are set using the `set_changeinfo` method.
-    
-    date, author, authenticated, ipnr:
-             date and authorship info for the event;
-             `date` is a datetime instance
-
-    Other properties:
-
-    href_fragment: optional fragment that will position to some place
-                   within the resource page
-
-    direct_href: direct link to the event,, if there's no resource associated
-                 to it
-    """
-
-    def __init__(self, *args, **kwargs):
-        """`TimelineEvent(provider, kind)` creates an event.
-
-        `provider` is the Component which provided the event and
-        `kind` is the specific sub-type of this event.
-
-        Note that 0.11dev API introduced originally another signature:
-        `(self, kind, title='', href=None, markup=None)`
-        We'll also stay compatible with the above until 0.12.
-        """
-        self.markup = {}
-        self.wikitext = {}
-        self.author = 'unknown'
-        self.date = self.authenticated = self.ipnr = None
-        self.resource = None
-        self.href_fragment = ''
-        if isinstance(args[0], Component):
-            self.provider = args[0]
-            self.kind = args[1]
-            self.env = self.provider.env
-        else:
-            self.kind = args[0]
-            class DummyProvider(object):
-                def event_formatter(self, event, key):
-                    return ('oneliner', {'shorten': True})
-            self.provider = DummyProvider()
-            title = len(args) > 1 and args[1] or kwargs.get('title')
-            href = len(args) > 2 and args[2] or kwargs.get('href')
-            markup = len(args) > 3 and args[3] or kwargs.get('markup')
-            self.direct_href = href
-            if title:
-                self.markup['title'] = title
-            if markup:
-                self.markup['header'] = markup
-
-    def get_href(self, href=None):
-        if self.resource:
-            return get_url(self.env, self.resource, href) + self.href_fragment
-        else:
-            return self.direct_href
-
-    def __repr__(self):
-        return '<TimelineEvent %s - %r>' % (self.date,
-                                            self.resource or self.direct_href)
-
-    def set_changeinfo(self, date, author='anonymous', authenticated=None,
-                       ipnr=None):
-        self.date = date
-        self.author = author
-        self.authenticated = authenticated
-        self.ipnr = ipnr
-
-    def add_markup(self, **kwargs):
-        """Populate the markup dictionary."""
-        for k, v in kwargs.iteritems():
-            if v:
-                self.markup[k] = v
-
-    def add_wiki(self, resource, **kwargs):
-        """Populate the wikitext dictionary."""
-        self.resource = resource
-        for k, v in kwargs.iteritems():
-            if v:
-                self.wikitext[k] = v
-
-    def dateuid(self):
-        return to_timestamp(self.date)
-
-
-
 class ITimelineEventProvider(Interface):
     """Extension point interface for adding sources for timed events to the
     timeline.
@@ -161,17 +48,31 @@ class ITimelineEventProvider(Interface):
         The `filters` parameters is a list of the enabled filters, each item
         being the name of the tuples returned by `get_timeline_filters`.
 
-        Since 0.11, the events are TimelineEvent instances.
+        Since 0.11, the events are `(kind, date, author, data)` tuples,
+        where `kind` is a string used for categorizing the event, `date`
+        is a `datetime` object, `author` is a string and `data` is some
+        private data that the component will reuse when rendering the event.
 
-        Note:
-        The events returned by this function used to be tuples of the form
-        (kind, href, title, date, author, markup). This is now deprecated.
+        When the event has been created indirectly by another module,
+        like this happens when calling `AttachmentModule.get_timeline_events()`
+        the tuple can also specify explicitly the provider by returning tuples
+        of the following form: `(kind, date, author, data, provider)`.
+
+        Before version 0.11,  the events returned by this function used to
+        be tuples of the form `(kind, href, title, date, author, markup)`.
+        This is still supported but less flexible, as `href`, `title` and
+        `markup` are not context dependent.
         """
 
-    def event_formatter(event, wikitext_key):
-        """For a given key (as found in the TimelineEvent.wikitext dictionary),
-        specify which formatter flavor and options should be used.
+    def render_timeline_event(context, field, event):
+        """Display the title of the event in the given context.
 
-        Returning `('oneliner', {})` is a safe choice and returning `None`
-        will let the template decide.
+        :param context: the rendering `Context` object that can be used for
+                        rendering
+        :param field: what specific part information from the event should
+                      be rendered: can be the 'title', the 'description' or
+                      the 'url'
+        :param event: the event tuple, as returned by `get_timeline_events`
         """
+
+
diff -r 00135419c47f -r f08a0d1b8960 trac/timeline/templates/timeline.html
--- a/trac/timeline/templates/timeline.html	Mon Oct 22 12:25:11 2007 +0200
+++ b/trac/timeline/templates/timeline.html	Mon Oct 22 12:34:10 2007 +0200
@@ -42,23 +42,13 @@
           <py:for each="event in events"
             py:with="highlight = precision and precisedate and timedelta(0) &lt;= (event.date - precisedate) &lt; precision">
             <dt class="${classes(event.kind, highlight=highlight)}">
-              <a href="${event.get_href(href)}">
-                <span class="time">${format_time(event.date, str('%H:%M'))}</span> ${event.markup.get('title')}
+              <a href="${event.render('url', context)}">
+                <span class="time">${format_time(event.date, str('%H:%M'))}</span> ${event.render('title', context)}
                 <py:if test="event.author">by <span class="author">${format_author(event.author)}</span></py:if>
               </a>
             </dt>
-            <!--! TODO: move the generation of the description back into the components -->
             <dd class="${classes(event.kind, highlight=highlight)}">
-              ${event.markup.get('header')}
-              <py:for each="key in event.wikitext">
-                <py:with vars="flavor, options = event.provider.event_formatter(event, key) or ('oneliner', {})"><?python
-                  if flavor == 'oneliner' and 'shorten' not in options:
-                      options['shorten'] = abbreviated_messages
-                  ?>
-                  ${wiki_to(flavor, context(event.resource), event.wikitext[key], **options)}
-                </py:with>
-              </py:for>
-              ${event.markup.get('footer')}
+              ${event.render('description', context)}
             </dd>
           </py:for>
         </dl>
diff -r 00135419c47f -r f08a0d1b8960 trac/timeline/templates/timeline.rss
--- a/trac/timeline/templates/timeline.rss	Mon Oct 22 12:25:11 2007 +0200
+++ b/trac/timeline/templates/timeline.rss	Mon Oct 22 12:34:10 2007 +0200
@@ -11,24 +11,24 @@
     <generator>Trac ${trac.version}</generator>
     <image py:if="chrome.logo.src">
       <title>${project.name}</title>
-      <url>${chrome.logo.src_abs and chrome.logo.src \
+      <url>${chrome.logo.src_abs and chrome.logo.src
                                  or abs_href(chrome.logo.src)}</url>
       <link>${abs_href.timeline()}</link>
     </image>
 
     <item py:for="event in events">
-      <title>${plaintext(event.markup.get('summary') or event.markup.get('title'), keeplinebreaks=False)}</title>
+      <title>${plaintext(event.render('summary', context) or 
+                         event.render('title', context), keeplinebreaks=False)}</title>
       ${author_or_creator(event.author, email_map)}
-      <pubDate>${http_date(event.date)}</pubDate>
-      <link>${event.get_href(abs_href)}</link>
-      <guid isPermaLink="false">${event.get_href(abs_href)}/${event.dateuid()}</guid>
-      <!--! TODO: move the generation of the description back to the components -->
+      <py:with vars="abs_url = event.render('url', abs_context)">
+        <pubDate>${http_date(event.date)}</pubDate>
+        <link>${abs_url}</link>
+      </py:with>
+      <guid isPermaLink="false">${abs_url}/${event.dateuid()}</guid>
       <description>${
-        unicode(event.markup.get('header', ''))
-      }<py:if test="'body' in event.wikitext">${unicode(wiki_to_html(context(event.resource, href=abs_href), event.wikitext.get('body')))}</py:if>${
-        unicode(event.markup.get('footer', ''))
+        unicode(event.render('description', abs_context)
       }</description>
-       <category>$event.kind</category>
+      <category>$event.kind</category>
     </item>
 
    </channel>
diff -r 00135419c47f -r f08a0d1b8960 trac/timeline/web_ui.py
--- a/trac/timeline/web_ui.py	Mon Oct 22 12:25:11 2007 +0200
+++ b/trac/timeline/web_ui.py	Mon Oct 22 12:34:10 2007 +0200
@@ -28,7 +28,7 @@ from trac.config import IntOption, BoolO
 from trac.config import IntOption, BoolOption
 from trac.core import *
 from trac.perm import IPermissionRequestor
-from trac.timeline.api import ITimelineEventProvider, TimelineEvent
+from trac.timeline.api import ITimelineEventProvider
 from trac.util.compat import sorted
 from trac.util.datefmt import format_date, format_datetime, parse_date, \
                               to_timestamp, utc, pretty_timedelta
@@ -122,8 +122,8 @@ class TimelineModule(Component):
         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: req.session.get('timeline.filter.%s' % f[0],
+                                               '') == '1',
                      lambda f: len(f) == 2 or f[2]):
             if filters:
                 break
@@ -147,21 +147,18 @@ class TimelineModule(Component):
             try:
                 for event in provider.get_timeline_events(req, start, stop,
                                                           filters):
-                    # compatibility with 0.10 providers
-                    if isinstance(event, tuple):
-                        event = self._event_from_tuple(req, event)
-                    events.append(event)
+                    events.append(self._event_data(provider, event))
             except Exception, e: # cope with a failure of that provider
                 self._provider_failure(e, req, provider, filters,
                                        [f[0] for f in available_filters])
 
-        events = sorted(events, key=lambda e: e.date, reverse=True)
-
         # prepare sorted global list
+        events = sorted(events, key=lambda e: e['date'], reverse=True)
         if maxrows:
-            data['events'] = events[:maxrows]
-        else:
-            data['events'] = events
+            events = events[:maxrows]
+
+        data['events'] = events
+        
 
         if format == 'rss':
             # Get the email addresses of all known users
@@ -260,17 +257,27 @@ class TimelineModule(Component):
 
     # Internal methods
 
-    def _event_from_tuple(self, req, event):
-        """Build a TimelineEvent from a pre-0.11 ITimelineEventProvider tuple
-        """
-        kind, href, title, date, author, markup = event
-        if not isinstance(date, datetime):
+    def _event_data(self, provider, event):
+        """Compose the timeline event date from the event tuple and prepared
+        provider methods"""
+        if len(event) == 6: # 0.10 events
+            kind, href, title, date, author, markup = event
+            fields = {'href': href, 'title': title, 'description': markup}
+            render = lambda field, context: fields.get(field)
+        else: # 0.11 events
+            if len(event) == 5: # with special provider
+                kind, date, author, data, provider = event
+            else:
+                kind, date, author, data = event
+            render = lambda field, context: provider.render_timeline_event(
+                context, field, event)
+        if isinstance(date, datetime):
+            dateuid = to_timestamp(date)
+        else:
+            dateuid = date
             date = datetime.fromtimestamp(date, utc)
-        if href and href.startswith(req.abs_href.base):
-            href = urlparse(href)[2]
-        event = TimelineEvent(kind, title, href, markup)
-        event.set_changeinfo(date, author)
-        return event
+        return {'kind': kind, 'author': author, 'date': date,
+                'dateuid': dateuid, 'render': render}
 
     def _provider_failure(self, exc, req, ep, current_filters, all_filters):
         """Raise a TracError exception explaining the failure of a provider.
diff -r 00135419c47f -r f08a0d1b8960 trac/versioncontrol/web_ui/changeset.py
--- a/trac/versioncontrol/web_ui/changeset.py	Mon Oct 22 12:25:11 2007 +0200
+++ b/trac/versioncontrol/web_ui/changeset.py	Mon Oct 22 12:34:10 2007 +0200
@@ -33,7 +33,7 @@ from trac.mimeview import Mimeview, is_b
 from trac.mimeview import Mimeview, is_binary
 from trac.perm import IPermissionRequestor
 from trac.search import ISearchSource, search_to_sql, shorten_result
-from trac.timeline.api import ITimelineEventProvider, TimelineEvent
+from trac.timeline.api import ITimelineEventProvider
 from trac.util import embedded_numbers, content_disposition
 from trac.util.compat import any, sorted, groupby
 from trac.util.datefmt import pretty_timedelta, utc
@@ -47,6 +47,7 @@ from trac.web.chrome import add_link, ad
 from trac.web.chrome import add_link, add_script, add_stylesheet, \
                             INavigationContributor, Chrome
 from trac.wiki import IWikiSyntaxProvider, WikiParser
+from trac.wiki.formatter import format_to_html
 
 
 class IPropertyDiffRenderer(Interface):
@@ -763,8 +764,6 @@ class ChangesetModule(Component):
                 show_files = int(show_files)
             else:
                 show_files = 0 # disabled
-            wiki_format = self.wiki_format_messages
-            long_messages = self.timeline_long_messages
             
             repos = self.env.get_repository(req.authname)
 
@@ -775,70 +774,76 @@ class ChangesetModule(Component):
                 
             for _, changesets in groupby(repos.get_changesets(start, stop),
                                          key=collapse_changesets):
-                changesets = list(changesets)
-                chgset = changesets[-1]
-                if not 'CHANGESET_VIEW' in req.perm('changeset', chgset.rev):
-                    continue
-                summary = shorten_line(chgset.message or '')
-                
-                if len(changesets) > 1:
-                    revs = '%s-%s' % (changesets[0].rev, changesets[-1].rev)
-                    title = tag('Changesets ', tag.em('[', revs, ']'))
-                    href = req.href.log(revs=revs)
-                else:
-                    title = tag('Changeset ', tag.em('[%s]' % chgset.rev))
-                    href = req.href.changeset(chgset.rev)
-                if wiki_format:
-                    message = chgset.message
-                    markup = ''
-                else:
-                    message = None
-                    markup = long_messages and chgset.message or summary
-                             
-
-                if 'BROWSER_VIEW' in req.perm:
-                    files = []
-                    if show_location:
-                        filestats = self._prepare_filestats()
-                        for c in changesets:
-                            for chg in c.get_changes():
-                                filestats[chg[2]] += 1
-                                files.append(chg[0])
-                        markup = tag.ul(tag.li(
-                            [(tag.div(class_=kind),
+                permitted_changesets = []
+                for chgset in changesets:
+                    if 'CHANGESET_VIEW' in req.perm('changeset', chgset.rev):
+                        permitted_changesets.append(chgset)
+                if permitted_changesets:
+                    chgset = permitted_changesets[-1]
+                    yield ('changeset', chgset.date, chgset.author,
+                           (permitted_changesets, chgset.message or '',
+                            show_location, show_files))
+
+    def render_timeline_event(self, context, field, event):
+        changesets, message, show_location, show_files = event[3]
+        rev_a, rev_b =  changesets[0].rev, changesets[-1].rev
+        
+        if field == 'url':
+            if rev_a == rev_b:
+                return context.href.changeset(rev_a)
+            else:
+                return context.href.log('@%s:%s' % (rev_a, rev_b))
+            
+        elif field == 'description':
+            if self.timeline_long_messages:
+                message = shorten_line(message)
+            if self.wiki_format_messages:
+                markup = ''
+            else:
+                markup = message
+                message = None
+            if 'BROWSER_VIEW' in context.perm:
+                files = []
+                if show_location:
+                    filestats = self._prepare_filestats()
+                    for c in changesets:
+                        for chg in c.get_changes():
+                            filestats[chg[2]] += 1
+                            files.append(chg[0])
+                    stats = [(tag.div(class_=kind),
                               tag.span(count, ' ',
                                        count > 1 and
                                        (kind == 'copy' and
                                         'copies' or kind + 's') or kind))
                              for kind in Changeset.ALL_CHANGES
-                             for count in (filestats[kind],) if count],
-                            ' in ', tag.strong(self._get_location(files))),
-                            markup, class_="changes")
-                    elif show_files:
-                        for c in changesets:
-                            for chg in c.get_changes():
-                                if show_files > 0 and len(files) > show_files:
-                                    break
-                                files.append(tag.li(tag.div(class_=chg[2]),
-                                                    chg[0] or '/'))
-                        if show_files > 0 and len(files) > show_files:
-                            files = files[:show_files] + [tag.li(u'\u2026')]
-                        markup = tag(tag.ul(files, class_="changes"), markup)
-
-                event = TimelineEvent(self, 'changeset')
-                event.add_markup(title=title, header=markup,
-                                 summary='%s: %s' % (title, summary))
-                event.set_changeinfo(chgset.date, chgset.author, True)
-                event.add_wiki(Resource('changeset', chgset.rev),
-                               body=message)
-                yield event
-
-    def event_formatter(self, event, key):
-        flavor = self.timeline_long_messages and 'default' or 'oneliner'
-        options = {}
-        if flavor == 'oneliner':
-            options['shorten'] = True
-        return (flavor, options)
+                             for count in (filestats[kind],) if count]
+                    markup = tag.ul(
+                        tag.li(stats, ' in ',
+                               tag.strong(self._get_location(files))),
+                        markup, class_="changes")
+                elif show_files:
+                    for c in changesets:
+                        for chg in c.get_changes():
+                            if show_files > 0 and len(files) > show_files:
+                                break
+                            files.append(tag.li(tag.div(class_=chg[2]),
+                                                chg[0] or '/'))
+                    if show_files > 0 and len(files) > show_files:
+                        files = files[:show_files] + [tag.li(u'\u2026')]
+                    markup = tag(tag.ul(files, class_="changes"), markup)
+            if message:
+                markup += format_to_html(context, message)
+            return markup
+
+        if rev_a == rev_b:
+            title = tag('Changeset ', tag.em('[%s]' % rev_a))
+        else:
+            title = tag('Changesets ', tag.em('[', rev_a, '-', rev_b, ']'))
+            
+        if field == 'title':
+            return title
+        elif field == 'summary':
+            return '%s: %s' % (title, shorten_line(message))
         
     # IWikiSyntaxProvider methods
 
diff -r 00135419c47f -r f08a0d1b8960 trac/wiki/formatter.py
--- a/trac/wiki/formatter.py	Mon Oct 22 12:25:11 2007 +0200
+++ b/trac/wiki/formatter.py	Mon Oct 22 12:34:10 2007 +0200
@@ -1019,17 +1019,17 @@ def format_to(flavor, context, wikidom, 
 
 def format_to_html(context, wikidom, escape_newlines=False):
     if not wikidom:
-        return ''
+        return Markup()
     return HtmlFormatter(context, wikidom).generate(escape_newlines)
 
 def format_to_oneliner(context, wikidom, shorten=False):
     if not wikidom:
-        return ''
+        return Markup()
     return InlineHtmlFormatter(context, wikidom).generate(shorten)
 
 def extract_link(context, wikidom):
     if not wikidom:
-        return ''
+        return Markup()
     return LinkFormatter(context).match(wikidom)
 
 
diff -r 00135419c47f -r f08a0d1b8960 trac/wiki/web_ui.py
--- a/trac/wiki/web_ui.py	Mon Oct 22 12:25:11 2007 +0200
+++ b/trac/wiki/web_ui.py	Mon Oct 22 12:34:10 2007 +0200
@@ -28,7 +28,7 @@ from trac.mimeview.api import Mimeview, 
 from trac.mimeview.api import Mimeview, IContentConverter
 from trac.perm import IPermissionRequestor
 from trac.search import ISearchSource, search_to_sql, shorten_result
-from trac.timeline.api import ITimelineEventProvider, TimelineEvent
+from trac.timeline.api import ITimelineEventProvider
 from trac.util import get_reporter_id
 from trac.util.datefmt import to_timestamp, utc
 from trac.util.text import shorten_line
@@ -38,6 +38,7 @@ from trac.web.chrome import add_link, ad
                             INavigationContributor, ITemplateProvider
 from trac.web import IRequestHandler
 from trac.wiki.api import IWikiPageManipulator, WikiSystem
+from trac.wiki.formatter import format_to_oneliner
 from trac.wiki.model import WikiPage
 
 class InvalidWikiPage(TracError):
@@ -521,33 +522,37 @@ class WikiModule(Component):
         if 'wiki' in filters:
             wiki_realm = Resource('wiki')
             cursor = db.cursor()
-            cursor.execute("SELECT time,name,comment,author,ipnr,version "
+            cursor.execute("SELECT time,name,comment,author,version "
                            "FROM wiki WHERE time>=%s AND time<=%s",
                            (to_timestamp(start), to_timestamp(stop)))
-            for ts,name,comment,author,ipnr,version in cursor:
-                p = wiki_realm(id=name, version=version)
-                if 'WIKI_VIEW' not in req.perm(p):
+            for ts,name,comment,author,version in cursor:
+                wiki_page = wiki_realm(id=name, version=version)
+                if 'WIKI_VIEW' not in req.perm(wiki_page):
                     continue
-                title = tag(tag.em(get_name(self.env, p)),
-                            version > 1 and ' edited' or ' created')
-                markup = None
-                if version > 1:
-                    markup = tag.a('(diff)',
-                                   href=req.href.wiki(p.id, action='diff'))
-                t = datetime.fromtimestamp(ts, utc)
-                event = TimelineEvent(self, 'wiki')
-                event.set_changeinfo(t, author, ipnr=ipnr)
-                event.add_markup(title=title, footer=markup)
-                event.add_wiki(p, body=comment)
-                yield event
+                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):
                 yield event
 
-    def event_formatter(self, event, key):
-        return None
+    def render_timeline_event(self, context, field, event):
+        wiki_page, comment = event[3]
+        if field == 'url':
+            return context.href.wiki(wiki_page.id, version=wiki_page.version)
+        elif field == 'title':
+            return tag(tag.em(get_name(self.env, wiki_page)),
+                       wiki_page.version > 1 and ' edited' or ' created')
+        elif field == 'description':
+            if self.config['timeline'].getbool('abbreviated_messages'):
+                comment = shorten_line(comment)
+            markup = format_to_oneliner(context(resource=wiki_page), comment)
+            if wiki_page.version > 1:
+                diff_href = context.href.wiki(
+                    wiki_page.id, version=wiki_page.version, action='diff')
+                markup = tag(markup, tag.a('(diff)', href=diff_href))
+            return markup
 
     # ISearchSource methods
 

