Edgewall Software

ChristianBoos: timeline-refactoring.patch

File timeline-refactoring.patch, 39.4 KB (added by cboos, 4 years ago)

clean-up the API changes that were introduced during 0.11dev.

  • trac/attachment.py

    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 b from trac.env import IEnvironmentSetupPa 
    3232from trac.env import IEnvironmentSetupParticipant 
    3333from trac.perm import PermissionError, PermissionSystem, IPermissionPolicy 
    3434from trac.mimeview import * 
    35 from trac.timeline.api import TimelineEvent 
    3635from trac.util import get_reporter_id, create_unique_file, content_disposition 
    3736from trac.util.datefmt import to_timestamp, utc 
    3837from trac.util.text import unicode_quote, unicode_unquote, pretty_size 
    from trac.web.chrome import add_link, ad 
    4140from trac.web.chrome import add_link, add_stylesheet, INavigationContributor 
    4241from trac.web.href import Href 
    4342from trac.wiki.api import IWikiSyntaxProvider 
     43from trac.wiki.formatter import format_to_oneliner 
    4444 
    4545 
    4646class InvalidAttachment(TracError): 
    class AttachmentModule(Component): 
    431431        """ 
    432432        for change, realm, id, filename, time, descr, author in \ 
    433433                self.get_history(start, stop, resource_realm.realm): 
    434             parent = resource_realm(id=id) 
    435             attachment = parent.child('attachment', filename) 
     434            attachment = resource_realm(id=id).child('attachment', filename) 
    436435            if 'ATTACHMENT_VIEW' in req.perm(attachment): 
    437                 title = tag(tag.em(os.path.basename(filename)), 
    438                             _(" attached to "), 
    439                             tag.em(get_name(self.env, parent), 
    440                                    title=get_summary(self.env, parent))) 
    441                 ### FIXME: the link is no longer to the attachment... 
    442                 event = TimelineEvent(self, 'attachment') 
    443                 event.set_changeinfo(time, author) 
    444                 event.add_markup(title=title) 
    445                 event.add_wiki(parent, body=descr) 
    446                 yield event 
    447  
    448     def event_formatter(self, event, key): 
    449         return None 
     436                yield ('attachment', time, author, (attachment, descr), self) 
     437 
     438    def render_timeline_event(self, context, field, event): 
     439        attachment, descr = event[3] 
     440        if field == 'url': 
     441            return self.get_resource_url(attachment, context.href) 
     442        elif field == 'title': 
     443            return tag(tag.em(os.path.basename(attachment.id)), 
     444                       _(" attached to "), 
     445                       tag.em(get_name(self.env, attachment.parent), 
     446                              title=get_summary(self.env, attachment.parent))) 
     447        elif field == 'description': 
     448            return format_to_oneliner(context(attachment.parent), descr) 
    450449     
    451450    # IResourceManager methods 
    452451     
  • trac/ticket/roadmap.py

    diff -r 00135419c47f -r f08a0d1b8960 trac/ticket/roadmap.py
    a b from genshi.builder import tag 
    2323 
    2424from trac import __version__ 
    2525from trac.attachment import AttachmentModule 
     26from trac.config import ExtensionOption 
    2627from trac.context import * 
    2728from trac.core import * 
    2829from trac.perm import IPermissionRequestor 
    from trac.util.translation import _ 
    3435from trac.util.translation import _ 
    3536from trac.ticket import Milestone, Ticket, TicketSystem 
    3637from trac.ticket.query import Query 
    37 from trac.timeline.api import ITimelineEventProvider, TimelineEvent 
     38from trac.timeline.api import ITimelineEventProvider 
    3839from trac.web import IRequestHandler 
    3940from trac.web.chrome import add_link, add_stylesheet, INavigationContributor 
    4041from trac.wiki.api import IWikiSyntaxProvider 
    41 from trac.config import ExtensionOption 
     42from trac.wiki.formatter import format_to_html 
    4243 
    4344class ITicketGroupStatsProvider(Interface): 
    4445    def get_ticket_group_stats(self, ticket_ids): 
    class MilestoneModule(Component): 
    506507            cursor.execute("SELECT completed,name,description FROM milestone " 
    507508                           "WHERE completed>=%s AND completed<=%s", 
    508509                           (to_timestamp(start), to_timestamp(stop))) 
    509             for ts, name, description in cursor: 
     510            for completed, name, description in cursor: 
    510511                milestone = milestone_realm(id=name) 
    511512                if 'MILESTONE_VIEW' in req.perm(milestone): 
    512                     completed = datetime.fromtimestamp(ts, utc) 
    513                     title = tag('Milestone ', tag.em(name), ' completed') 
    514                     event = TimelineEvent('milestone', title, 
    515                                           req.href.milestone(name)) 
    516                     event.set_changeinfo(completed, '') # FIXME: store the author 
    517                     event.add_wiki(milestone, description) # FIXME xxxo 
    518                     yield event 
     513                    yield('milestone', datetime.fromtimestamp(completed, utc), 
     514                          '', (milestone, description)) # FIXME: author? 
    519515 
    520516            # Attachments 
    521517            for event in AttachmentModule(self.env).get_timeline_events( 
    522518                req, milestone_realm, start, stop): 
    523519                yield event 
    524520                 
    525  
    526     def event_formatter(self, event, key): 
    527         return None 
     521    def render_timeline_event(self, context, field, event): 
     522        milestone, description = event[3] 
     523        if field == 'url': 
     524            return context.href.milestone(milestone.id) 
     525        elif field == 'title': 
     526            return tag('Milestone ', tag.em(milestone.id), ' completed') 
     527        elif field == 'description': 
     528            return format_to_html(context(resource=milestone), 
     529                                  shorten_line(description)) 
    528530 
    529531    # IRequestHandler methods 
    530532 
  • trac/ticket/web_ui.py

    diff -r 00135419c47f -r f08a0d1b8960 trac/ticket/web_ui.py
    a b from trac.ticket import Milestone, Ticke 
    3333from trac.ticket import Milestone, Ticket, TicketSystem, ITicketManipulator 
    3434from trac.ticket import ITicketActionController 
    3535from trac.ticket.notification import TicketNotifyEmail 
    36 from trac.timeline.api import ITimelineEventProvider, TimelineEvent 
     36from trac.timeline.api import ITimelineEventProvider 
    3737from trac.util import get_reporter_id 
    3838from trac.util.compat import any 
    3939from trac.util.datefmt import to_timestamp, utc 
    from trac.web import IRequestHandler 
    4444from trac.web import IRequestHandler 
    4545from trac.web.chrome import add_link, add_script, add_stylesheet, Chrome, \ 
    4646                            INavigationContributor, ITemplateProvider 
    47  
     47from trac.wiki.formatter import format_to 
    4848 
    4949class InvalidTicket(TracError): 
    5050    """Exception raised when a ticket fails validation.""" 
    class TicketModule(Component): 
    204204                      'reopened': ('reopenedticket', 'reopened'), 
    205205                      'closed': ('closedticket', 'closed'), 
    206206                      'edit': ('editedticket', 'updated')} 
     207 
    207208        ticket_realm = Resource('ticket') 
    208         ticketsystem = TicketSystem(self.env) 
    209         description = {} 
    210  
    211         def produce((id, ts, author, type, summary, description), 
    212                     status, fields, comment, cid): 
    213             resource = ticket_realm(id=id) 
    214             if 'TICKET_VIEW' not in req.perm(resource): 
     209 
     210        def produce_event((id, ts, author, type, summary, description), 
     211                          status, fields, comment, cid): 
     212            ticket = ticket_realm(id=id) 
     213            if 'TICKET_VIEW' not in req.perm(ticket): 
    215214                return None 
     215            resolution = fields.get('resolution') 
    216216            info = '' 
    217             resolution = fields.get('resolution') 
    218217            if status == 'edit': 
    219218                if 'ticket_details' in filters: 
    220219                    if len(fields) > 0: 
    class TicketModule(Component): 
    231230            else: 
    232231                return None 
    233232            kind, verb = status_map[status] 
    234             title = ticketsystem.format_summary(summary, status, 
    235                                                 resolution, type) 
    236             title = tag('Ticket ', tag.em(get_shortname(self.env, resource), 
    237                                           title=title), 
    238                         ' (', shorten_line(summary), ') ', verb) 
    239             markup = message = None 
    240             if status == 'new': 
    241                 message = description 
    242             else: 
    243                 markup = info 
    244                 message = comment 
    245             t = datetime.fromtimestamp(ts, utc) 
    246             event = TimelineEvent(self, kind) 
    247             event.set_changeinfo(t, author) 
    248             event.add_markup(title=title, header=markup) # FIXME 
    249             event.add_wiki(resource, body=message) # FIXME 
    250             if cid: 
    251                 event.href_fragment = '#comment:' + cid 
    252             return event 
     233            return (kind, datetime.fromtimestamp(ts, utc), author, 
     234                    (ticket, verb, info, summary, status, resolution, type, 
     235                     description, comment, cid)) 
    253236 
    254237        # Ticket changes 
    255238        db = self.env.get_db_cnx() 
    class TicketModule(Component): 
    267250            for id,t,author,type,summary,field,oldvalue,newvalue in cursor: 
    268251                if not previous_update or (id,t,author) != previous_update[:3]: 
    269252                    if previous_update: 
    270                         ev = produce(previous_update, status, fields, 
    271                                      comment, cid) 
     253                        ev = produce_event(previous_update, status, fields, 
     254                                           comment, cid) 
    272255                        if ev: 
    273256                            yield ev 
    274257                    status, fields, comment, cid = 'edit', {}, '', None 
    class TicketModule(Component): 
    281264                else: 
    282265                    fields[field] = newvalue 
    283266            if previous_update: 
    284                 ev = produce(previous_update, status, fields, comment, cid) 
     267                ev = produce_event(previous_update, status, fields, 
     268                                   comment, cid) 
    285269                if ev: 
    286270                    yield ev 
    287271 
    class TicketModule(Component): 
    292276                               "  FROM ticket WHERE time>=%s AND time<=%s", 
    293277                               (ts_start, ts_stop)) 
    294278                for row in cursor: 
    295                     ev = produce(row, 'new', {}, None, None) 
     279                    ev = produce_event(row, 'new', {}, None, None) 
    296280                    if ev: 
    297281                        yield ev 
    298282 
    class TicketModule(Component): 
    302286                    req, ticket_realm, start, stop): 
    303287                    yield event 
    304288 
    305     def event_formatter(self, event, key): 
    306         flavor = 'oneliner' 
    307         if event.kind == 'newticket': 
    308             flavor = self.timeline_newticket_formatter 
    309         return (flavor, {}) 
     289    def render_timeline_event(self, context, field, event): 
     290        ticket, verb, info, summary, status, resolution, type, \ 
     291                description, comment, cid = event[3] 
     292        if field == 'url': 
     293            href = context.href.ticket(ticket.id) 
     294            if cid: 
     295                href += '#comment:' + cid 
     296            return href 
     297        elif field == 'title': 
     298            title = TicketSystem(self.env).format_summary(summary, status, 
     299                                                          resolution, type) 
     300            return tag('Ticket ', tag.em('#', ticket.id, title=title), 
     301                       ' (', shorten_line(summary), ') ', verb) 
     302        elif field == 'description': 
     303            descr = message = '' 
     304            if status == 'new': 
     305                message = description 
     306                flavor = self.timeline_newticket_formatter 
     307            else: 
     308                descr = info 
     309                message = comment 
     310                flavor = 'oneliner' 
     311            if message: 
     312                if self.config['timeline'].getbool('abbreviated_messages'): 
     313                    message = shorten_line(message) 
     314                descr += format_to(flavor, context(resource=ticket), message) 
     315            return descr 
    310316 
    311317    # Internal methods 
    312318 
  • trac/timeline/api.py

    diff -r 00135419c47f -r f08a0d1b8960 trac/timeline/api.py
    a b from trac.web.href import Href 
    2525from trac.web.href import Href 
    2626 
    2727 
    28 class TimelineEvent(object): 
    29     """Group event related information. 
    30  
    31     WARNING: this interface is going to be overhauled 
    32  
    33     The first two properties are set in the constructor: 
    34  
    35     provider: reference to the event provider 
    36  
    37     kind: category of the event, will also be used as the CSS class for 
    38           the event's entry in the timeline 
    39  
    40     The following is set using the `add_markup` method. 
    41      
    42     markup: dictionary of litteral informations regarding the events. 
    43             Standard keys include: 
    44              - 'title': short summary for the event 
    45              - 'header': markup that comes before the main body 
    46              - 'footer': markup that comes after the main body 
    47  
    48     The next two are set using the `add_wiki` method. 
    49      
    50     resource: resource context 
    51     wikitext: dictionary of contextual information 
    52               Standard keys include: 
    53               `body` will be interpreted as the main text 
    54               `summary` 
    55  
    56     The next four are set using the `set_changeinfo` method. 
    57      
    58     date, author, authenticated, ipnr: 
    59              date and authorship info for the event; 
    60              `date` is a datetime instance 
    61  
    62     Other properties: 
    63  
    64     href_fragment: optional fragment that will position to some place 
    65                    within the resource page 
    66  
    67     direct_href: direct link to the event,, if there's no resource associated 
    68                  to it 
    69     """ 
    70  
    71     def __init__(self, *args, **kwargs): 
    72         """`TimelineEvent(provider, kind)` creates an event. 
    73  
    74         `provider` is the Component which provided the event and 
    75         `kind` is the specific sub-type of this event. 
    76  
    77         Note that 0.11dev API introduced originally another signature: 
    78         `(self, kind, title='', href=None, markup=None)` 
    79         We'll also stay compatible with the above until 0.12. 
    80         """ 
    81         self.markup = {} 
    82         self.wikitext = {} 
    83         self.author = 'unknown' 
    84         self.date = self.authenticated = self.ipnr = None 
    85         self.resource = None 
    86         self.href_fragment = '' 
    87         if isinstance(args[0], Component): 
    88             self.provider = args[0] 
    89             self.kind = args[1] 
    90             self.env = self.provider.env 
    91         else: 
    92             self.kind = args[0] 
    93             class DummyProvider(object): 
    94                 def event_formatter(self, event, key): 
    95                     return ('oneliner', {'shorten': True}) 
    96             self.provider = DummyProvider() 
    97             title = len(args) > 1 and args[1] or kwargs.get('title') 
    98             href = len(args) > 2 and args[2] or kwargs.get('href') 
    99             markup = len(args) > 3 and args[3] or kwargs.get('markup') 
    100             self.direct_href = href 
    101             if title: 
    102                 self.markup['title'] = title 
    103             if markup: 
    104                 self.markup['header'] = markup 
    105  
    106     def get_href(self, href=None): 
    107         if self.resource: 
    108             return get_url(self.env, self.resource, href) + self.href_fragment 
    109         else: 
    110             return self.direct_href 
    111  
    112     def __repr__(self): 
    113         return '<TimelineEvent %s - %r>' % (self.date, 
    114                                             self.resource or self.direct_href) 
    115  
    116     def set_changeinfo(self, date, author='anonymous', authenticated=None, 
    117                        ipnr=None): 
    118         self.date = date 
    119         self.author = author 
    120         self.authenticated = authenticated 
    121         self.ipnr = ipnr 
    122  
    123     def add_markup(self, **kwargs): 
    124         """Populate the markup dictionary.""" 
    125         for k, v in kwargs.iteritems(): 
    126             if v: 
    127                 self.markup[k] = v 
    128  
    129     def add_wiki(self, resource, **kwargs): 
    130         """Populate the wikitext dictionary.""" 
    131         self.resource = resource 
    132         for k, v in kwargs.iteritems(): 
    133             if v: 
    134                 self.wikitext[k] = v 
    135  
    136     def dateuid(self): 
    137         return to_timestamp(self.date) 
    138  
    139  
    140  
    14128class ITimelineEventProvider(Interface): 
    14229    """Extension point interface for adding sources for timed events to the 
    14330    timeline. 
    class ITimelineEventProvider(Interface): 
    16148        The `filters` parameters is a list of the enabled filters, each item 
    16249        being the name of the tuples returned by `get_timeline_filters`. 
    16350 
    164         Since 0.11, the events are TimelineEvent instances. 
     51        Since 0.11, the events are `(kind, date, author, data)` tuples, 
     52        where `kind` is a string used for categorizing the event, `date` 
     53        is a `datetime` object, `author` is a string and `data` is some 
     54        private data that the component will reuse when rendering the event. 
    16555 
    166         Note: 
    167         The events returned by this function used to be tuples of the form 
    168         (kind, href, title, date, author, markup). This is now deprecated. 
     56        When the event has been created indirectly by another module, 
     57        like this happens when calling `AttachmentModule.get_timeline_events()` 
     58        the tuple can also specify explicitly the provider by returning tuples 
     59        of the following form: `(kind, date, author, data, provider)`. 
     60 
     61        Before version 0.11,  the events returned by this function used to 
     62        be tuples of the form `(kind, href, title, date, author, markup)`. 
     63        This is still supported but less flexible, as `href`, `title` and 
     64        `markup` are not context dependent. 
    16965        """ 
    17066 
    171     def event_formatter(event, wikitext_key): 
    172         """For a given key (as found in the TimelineEvent.wikitext dictionary), 
    173         specify which formatter flavor and options should be used. 
     67    def render_timeline_event(context, field, event): 
     68        """Display the title of the event in the given context. 
    17469 
    175         Returning `('oneliner', {})` is a safe choice and returning `None` 
    176         will let the template decide. 
     70        :param context: the rendering `Context` object that can be used for 
     71                        rendering 
     72        :param field: what specific part information from the event should 
     73                      be rendered: can be the 'title', the 'description' or 
     74                      the 'url' 
     75        :param event: the event tuple, as returned by `get_timeline_events` 
    17776        """ 
     77 
     78 
  • trac/timeline/templates/timeline.html

    diff -r 00135419c47f -r f08a0d1b8960 trac/timeline/templates/timeline.html
    a b  
    4242          <py:for each="event in events" 
    4343            py:with="highlight = precision and precisedate and timedelta(0) &lt;= (event.date - precisedate) &lt; precision"> 
    4444            <dt class="${classes(event.kind, highlight=highlight)}"> 
    45               <a href="${event.get_href(href)}"> 
    46                 <span class="time">${format_time(event.date, str('%H:%M'))}</span> ${event.markup.get('title')} 
     45              <a href="${event.render('url', context)}"> 
     46                <span class="time">${format_time(event.date, str('%H:%M'))}</span> ${event.render('title', context)} 
    4747                <py:if test="event.author">by <span class="author">${format_author(event.author)}</span></py:if> 
    4848              </a> 
    4949            </dt> 
    50             <!--! TODO: move the generation of the description back into the components --> 
    5150            <dd class="${classes(event.kind, highlight=highlight)}"> 
    52               ${event.markup.get('header')} 
    53               <py:for each="key in event.wikitext"> 
    54                 <py:with vars="flavor, options = event.provider.event_formatter(event, key) or ('oneliner', {})"><?python 
    55                   if flavor == 'oneliner' and 'shorten' not in options: 
    56                       options['shorten'] = abbreviated_messages 
    57                   ?> 
    58                   ${wiki_to(flavor, context(event.resource), event.wikitext[key], **options)} 
    59                 </py:with> 
    60               </py:for> 
    61               ${event.markup.get('footer')} 
     51              ${event.render('description', context)} 
    6252            </dd> 
    6353          </py:for> 
    6454        </dl> 
  • trac/timeline/templates/timeline.rss

    diff -r 00135419c47f -r f08a0d1b8960 trac/timeline/templates/timeline.rss
    a b  
    1111    <generator>Trac ${trac.version}</generator> 
    1212    <image py:if="chrome.logo.src"> 
    1313      <title>${project.name}</title> 
    14       <url>${chrome.logo.src_abs and chrome.logo.src \ 
     14      <url>${chrome.logo.src_abs and chrome.logo.src 
    1515                                 or abs_href(chrome.logo.src)}</url> 
    1616      <link>${abs_href.timeline()}</link> 
    1717    </image> 
    1818 
    1919    <item py:for="event in events"> 
    20       <title>${plaintext(event.markup.get('summary') or event.markup.get('title'), keeplinebreaks=False)}</title> 
     20      <title>${plaintext(event.render('summary', context) or  
     21                         event.render('title', context), keeplinebreaks=False)}</title> 
    2122      ${author_or_creator(event.author, email_map)} 
    22       <pubDate>${http_date(event.date)}</pubDate> 
    23       <link>${event.get_href(abs_href)}</link> 
    24       <guid isPermaLink="false">${event.get_href(abs_href)}/${event.dateuid()}</guid> 
    25       <!--! TODO: move the generation of the description back to the components --> 
     23      <py:with vars="abs_url = event.render('url', abs_context)"> 
     24        <pubDate>${http_date(event.date)}</pubDate> 
     25        <link>${abs_url}</link> 
     26      </py:with> 
     27      <guid isPermaLink="false">${abs_url}/${event.dateuid()}</guid> 
    2628      <description>${ 
    27         unicode(event.markup.get('header', '')) 
    28       }<py:if test="'body' in event.wikitext">${unicode(wiki_to_html(context(event.resource, href=abs_href), event.wikitext.get('body')))}</py:if>${ 
    29         unicode(event.markup.get('footer', '')) 
     29        unicode(event.render('description', abs_context) 
    3030      }</description> 
    31        <category>$event.kind</category> 
     31      <category>$event.kind</category> 
    3232    </item> 
    3333 
    3434   </channel> 
  • trac/timeline/web_ui.py

    diff -r 00135419c47f -r f08a0d1b8960 trac/timeline/web_ui.py
    a b from trac.config import IntOption, BoolO 
    2828from trac.config import IntOption, BoolOption 
    2929from trac.core import * 
    3030from trac.perm import IPermissionRequestor 
    31 from trac.timeline.api import ITimelineEventProvider, TimelineEvent 
     31from trac.timeline.api import ITimelineEventProvider 
    3232from trac.util.compat import sorted 
    3333from trac.util.datefmt import format_date, format_datetime, parse_date, \ 
    3434                              to_timestamp, utc, pretty_timedelta 
    class TimelineModule(Component): 
    122122        filters = [] 
    123123        # check the request or session for enabled filters, or use default 
    124124        for test in (lambda f: f[0] in req.args, 
    125                      lambda f: req.session.get('timeline.filter.%s' % f[0], '')\ 
    126                                == '1', 
     125                     lambda f: req.session.get('timeline.filter.%s' % f[0], 
     126                                               '') == '1', 
    127127                     lambda f: len(f) == 2 or f[2]): 
    128128            if filters: 
    129129                break 
    class TimelineModule(Component): 
    147147            try: 
    148148                for event in provider.get_timeline_events(req, start, stop, 
    149149                                                          filters): 
    150                     # compatibility with 0.10 providers 
    151                     if isinstance(event, tuple): 
    152                         event = self._event_from_tuple(req, event) 
    153                     events.append(event) 
     150                    events.append(self._event_data(provider, event)) 
    154151            except Exception, e: # cope with a failure of that provider 
    155152                self._provider_failure(e, req, provider, filters, 
    156153                                       [f[0] for f in available_filters]) 
    157154 
    158         events = sorted(events, key=lambda e: e.date, reverse=True) 
    159  
    160155        # prepare sorted global list 
     156        events = sorted(events, key=lambda e: e['date'], reverse=True) 
    161157        if maxrows: 
    162             data['events'] = events[:maxrows] 
    163         else: 
    164             data['events'] = events 
     158            events = events[:maxrows] 
     159 
     160        data['events'] = events 
     161         
    165162 
    166163        if format == 'rss': 
    167164            # Get the email addresses of all known users 
    class TimelineModule(Component): 
    260257 
    261258    # Internal methods 
    262259 
    263     def _event_from_tuple(self, req, event): 
    264         """Build a TimelineEvent from a pre-0.11 ITimelineEventProvider tuple 
    265         """ 
    266         kind, href, title, date, author, markup = event 
    267         if not isinstance(date, datetime): 
     260    def _event_data(self, provider, event): 
     261        """Compose the timeline event date from the event tuple and prepared 
     262        provider methods""" 
     263        if len(event) == 6: # 0.10 events 
     264            kind, href, title, date, author, markup = event 
     265            fields = {'href': href, 'title': title, 'description': markup} 
     266            render = lambda field, context: fields.get(field) 
     267        else: # 0.11 events 
     268            if len(event) == 5: # with special provider 
     269                kind, date, author, data, provider = event 
     270            else: 
     271                kind, date, author, data = event 
     272            render = lambda field, context: provider.render_timeline_event( 
     273                context, field, event) 
     274        if isinstance(date, datetime): 
     275            dateuid = to_timestamp(date) 
     276        else: 
     277            dateuid = date 
    268278            date = datetime.fromtimestamp(date, utc) 
    269         if href and href.startswith(req.abs_href.base): 
    270             href = urlparse(href)[2] 
    271         event = TimelineEvent(kind, title, href, markup) 
    272         event.set_changeinfo(date, author) 
    273         return event 
     279        return {'kind': kind, 'author': author, 'date': date, 
     280                'dateuid': dateuid, 'render': render} 
    274281 
    275282    def _provider_failure(self, exc, req, ep, current_filters, all_filters): 
    276283        """Raise a TracError exception explaining the failure of a provider. 
  • trac/versioncontrol/web_ui/changeset.py

    diff -r 00135419c47f -r f08a0d1b8960 trac/versioncontrol/web_ui/changeset.py
    a b from trac.mimeview import Mimeview, is_b 
    3333from trac.mimeview import Mimeview, is_binary 
    3434from trac.perm import IPermissionRequestor 
    3535from trac.search import ISearchSource, search_to_sql, shorten_result 
    36 from trac.timeline.api import ITimelineEventProvider, TimelineEvent 
     36from trac.timeline.api import ITimelineEventProvider 
    3737from trac.util import embedded_numbers, content_disposition 
    3838from trac.util.compat import any, sorted, groupby 
    3939from trac.util.datefmt import pretty_timedelta, utc 
    from trac.web.chrome import add_link, ad 
    4747from trac.web.chrome import add_link, add_script, add_stylesheet, \ 
    4848                            INavigationContributor, Chrome 
    4949from trac.wiki import IWikiSyntaxProvider, WikiParser 
     50from trac.wiki.formatter import format_to_html 
    5051 
    5152 
    5253class IPropertyDiffRenderer(Interface): 
    class ChangesetModule(Component): 
    763764                show_files = int(show_files) 
    764765            else: 
    765766                show_files = 0 # disabled 
    766             wiki_format = self.wiki_format_messages 
    767             long_messages = self.timeline_long_messages 
    768767             
    769768            repos = self.env.get_repository(req.authname) 
    770769 
    class ChangesetModule(Component): 
    775774                 
    776775            for _, changesets in groupby(repos.get_changesets(start, stop), 
    777776                                         key=collapse_changesets): 
    778                 changesets = list(changesets) 
    779                 chgset = changesets[-1] 
    780                 if not 'CHANGESET_VIEW' in req.perm('changeset', chgset.rev): 
    781                     continue 
    782                 summary = shorten_line(chgset.message or '') 
    783                  
    784                 if len(changesets) > 1: 
    785                     revs = '%s-%s' % (changesets[0].rev, changesets[-1].rev) 
    786                     title = tag('Changesets ', tag.em('[', revs, ']')) 
    787                     href = req.href.log(revs=revs) 
    788                 else: 
    789                     title = tag('Changeset ', tag.em('[%s]' % chgset.rev)) 
    790                     href = req.href.changeset(chgset.rev) 
    791                 if wiki_format: 
    792                     message = chgset.message 
    793                     markup = '' 
    794                 else: 
    795                     message = None 
    796                     markup = long_messages and chgset.message or summary 
    797                               
    798  
    799                 if 'BROWSER_VIEW' in req.perm: 
    800                     files = [] 
    801                     if show_location: 
    802                         filestats = self._prepare_filestats() 
    803                         for c in changesets: 
    804                             for chg in c.get_changes(): 
    805                                 filestats[chg[2]] += 1 
    806                                 files.append(chg[0]) 
    807                         markup = tag.ul(tag.li( 
    808                             [(tag.div(class_=kind), 
     777                permitted_changesets = [] 
     778                for chgset in changesets: 
     779                    if 'CHANGESET_VIEW' in req.perm('changeset', chgset.rev): 
     780                        permitted_changesets.append(chgset) 
     781                if permitted_changesets: 
     782                    chgset = permitted_changesets[-1] 
     783                    yield ('changeset', chgset.date, chgset.author, 
     784                           (permitted_changesets, chgset.message or '', 
     785                            show_location, show_files)) 
     786 
     787    def render_timeline_event(self, context, field, event): 
     788        changesets, message, show_location, show_files = event[3] 
     789        rev_a, rev_b =  changesets[0].rev, changesets[-1].rev 
     790         
     791        if field == 'url': 
     792            if rev_a == rev_b: 
     793                return context.href.changeset(rev_a) 
     794            else: 
     795                return context.href.log('@%s:%s' % (rev_a, rev_b)) 
     796             
     797        elif field == 'description': 
     798            if self.timeline_long_messages: 
     799                message = shorten_line(message) 
     800            if self.wiki_format_messages: 
     801                markup = '' 
     802            else: 
     803                markup = message 
     804                message = None 
     805            if 'BROWSER_VIEW' in context.perm: 
     806                files = [] 
     807                if show_location: 
     808                    filestats = self._prepare_filestats() 
     809                    for c in changesets: 
     810                        for chg in c.get_changes(): 
     811                            filestats[chg[2]] += 1 
     812                            files.append(chg[0]) 
     813                    stats = [(tag.div(class_=kind), 
    809814                              tag.span(count, ' ', 
    810815                                       count > 1 and 
    811816                                       (kind == 'copy' and 
    812817                                        'copies' or kind + 's') or kind)) 
    813818                             for kind in Changeset.ALL_CHANGES 
    814                              for count in (filestats[kind],) if count], 
    815                             ' in ', tag.strong(self._get_location(files))), 
    816                             markup, class_="changes") 
    817                     elif show_files: 
    818                         for c in changesets: 
    819                             for chg in c.get_changes(): 
    820                                 if show_files > 0 and len(files) > show_files: 
    821                                     break 
    822                                 files.append(tag.li(tag.div(class_=chg[2]), 
    823                                                     chg[0] or '/')) 
    824                         if show_files > 0 and len(files) > show_files: 
    825                             files = files[:show_files] + [tag.li(u'\u2026')] 
    826                         markup = tag(tag.ul(files, class_="changes"), markup) 
    827  
    828                 event = TimelineEvent(self, 'changeset') 
    829                 event.add_markup(title=title, header=markup, 
    830                                  summary='%s: %s' % (title, summary)) 
    831                 event.set_changeinfo(chgset.date, chgset.author, True) 
    832                 event.add_wiki(Resource('changeset', chgset.rev), 
    833                                body=message) 
    834                 yield event 
    835  
    836     def event_formatter(self, event, key): 
    837         flavor = self.timeline_long_messages and 'default' or 'oneliner' 
    838         options = {} 
    839         if flavor == 'oneliner': 
    840             options['shorten'] = True 
    841         return (flavor, options) 
     819                             for count in (filestats[kind],) if count] 
     820                    markup = tag.ul( 
     821                        tag.li(stats, ' in ', 
     822                               tag.strong(self._get_location(files))), 
     823                        markup, class_="changes") 
     824                elif show_files: 
     825                    for c in changesets: 
     826                        for chg in c.get_changes(): 
     827                            if show_files > 0 and len(files) > show_files: 
     828                                break 
     829                            files.append(tag.li(tag.div(class_=chg[2]), 
     830                                                chg[0] or '/')) 
     831                    if show_files > 0 and len(files) > show_files: 
     832                        files = files[:show_files] + [tag.li(u'\u2026')] 
     833                    markup = tag(tag.ul(files, class_="changes"), markup) 
     834            if message: 
     835                markup += format_to_html(context, message) 
     836            return markup 
     837 
     838        if rev_a == rev_b: 
     839            title = tag('Changeset ', tag.em('[%s]' % rev_a)) 
     840        else: 
     841            title = tag('Changesets ', tag.em('[', rev_a, '-', rev_b, ']')) 
     842             
     843        if field == 'title': 
     844            return title 
     845        elif field == 'summary': 
     846            return '%s: %s' % (title, shorten_line(message)) 
    842847         
    843848    # IWikiSyntaxProvider methods 
    844849 
  • trac/wiki/formatter.py

    diff -r 00135419c47f -r f08a0d1b8960 trac/wiki/formatter.py
    a b def format_to(flavor, context, wikidom,  
    10191019 
    10201020def format_to_html(context, wikidom, escape_newlines=False): 
    10211021    if not wikidom: 
    1022         return '' 
     1022        return Markup() 
    10231023    return HtmlFormatter(context, wikidom).generate(escape_newlines) 
    10241024 
    10251025def format_to_oneliner(context, wikidom, shorten=False): 
    10261026    if not wikidom: 
    1027         return '' 
     1027        return Markup() 
    10281028    return InlineHtmlFormatter(context, wikidom).generate(shorten) 
    10291029 
    10301030def extract_link(context, wikidom): 
    10311031    if not wikidom: 
    1032         return '' 
     1032        return Markup() 
    10331033    return LinkFormatter(context).match(wikidom) 
    10341034 
    10351035 
  • trac/wiki/web_ui.py

    diff -r 00135419c47f -r f08a0d1b8960 trac/wiki/web_ui.py
    a b from trac.mimeview.api import Mimeview,  
    2828from trac.mimeview.api import Mimeview, IContentConverter 
    2929from trac.perm import IPermissionRequestor 
    3030from trac.search import ISearchSource, search_to_sql, shorten_result 
    31 from trac.timeline.api import ITimelineEventProvider, TimelineEvent 
     31from trac.timeline.api import ITimelineEventProvider 
    3232from trac.util import get_reporter_id 
    3333from trac.util.datefmt import to_timestamp, utc 
    3434from trac.util.text import shorten_line 
    from trac.web.chrome import add_link, ad 
    3838                            INavigationContributor, ITemplateProvider 
    3939from trac.web import IRequestHandler 
    4040from trac.wiki.api import IWikiPageManipulator, WikiSystem 
     41from trac.wiki.formatter import format_to_oneliner 
    4142from trac.wiki.model import WikiPage 
    4243 
    4344class InvalidWikiPage(TracError): 
    class WikiModule(Component): 
    521522        if 'wiki' in filters: 
    522523            wiki_realm = Resource('wiki') 
    523524            cursor = db.cursor() 
    524             cursor.execute("SELECT time,name,comment,author,ipnr,version " 
     525            cursor.execute("SELECT time,name,comment,author,version " 
    525526                           "FROM wiki WHERE time>=%s AND time<=%s", 
    526527                           (to_timestamp(start), to_timestamp(stop))) 
    527             for ts,name,comment,author,ipnr,version in cursor: 
    528                 p = wiki_realm(id=name, version=version) 
    529                 if 'WIKI_VIEW' not in req.perm(p): 
     528            for ts,name,comment,author,version in cursor: 
     529                wiki_page = wiki_realm(id=name, version=version) 
     530                if 'WIKI_VIEW' not in req.perm(wiki_page): 
    530531                    continue 
    531                 title = tag(tag.em(get_name(self.env, p)), 
    532                             version > 1 and ' edited' or ' created') 
    533                 markup = None 
    534                 if version > 1: 
    535                     markup = tag.a('(diff)', 
    536                                    href=req.href.wiki(p.id, action='diff')) 
    537                 t = datetime.fromtimestamp(ts, utc) 
    538                 event = TimelineEvent(self, 'wiki') 
    539                 event.set_changeinfo(t, author, ipnr=ipnr) 
    540                 event.add_markup(title=title, footer=markup) 
    541                 event.add_wiki(p, body=comment) 
    542                 yield event 
     532                yield ('wiki', datetime.fromtimestamp(ts, utc), author, 
     533                       (wiki_page, comment)) 
    543534 
    544535            # Attachments 
    545536            for event in AttachmentModule(self.env).get_timeline_events( 
    546537                req, wiki_realm, start, stop): 
    547538                yield event 
    548539 
    549     def event_formatter(self, event, key): 
    550         return None 
     540    def render_timeline_event(self, context, field, event): 
     541        wiki_page, comment = event[3] 
     542        if field == 'url': 
     543            return context.href.wiki(wiki_page.id, version=wiki_page.version) 
     544        elif field == 'title': 
     545            return tag(tag.em(get_name(self.env, wiki_page)), 
     546                       wiki_page.version > 1 and ' edited' or ' created') 
     547        elif field == 'description': 
     548            if self.config['timeline'].getbool('abbreviated_messages'): 
     549                comment = shorten_line(comment) 
     550            markup = format_to_oneliner(context(resource=wiki_page), comment) 
     551            if wiki_page.version > 1: 
     552                diff_href = context.href.wiki( 
     553                    wiki_page.id, version=wiki_page.version, action='diff') 
     554                markup = tag(markup, tag.a('(diff)', href=diff_href)) 
     555            return markup 
    551556 
    552557    # ISearchSource methods 
    553558