ChristianBoos: timeline-refactoring.patch
| File timeline-refactoring.patch, 39.4 KB (added by cboos, 4 years ago) |
|---|
-
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 32 32 from trac.env import IEnvironmentSetupParticipant 33 33 from trac.perm import PermissionError, PermissionSystem, IPermissionPolicy 34 34 from trac.mimeview import * 35 from trac.timeline.api import TimelineEvent36 35 from trac.util import get_reporter_id, create_unique_file, content_disposition 37 36 from trac.util.datefmt import to_timestamp, utc 38 37 from trac.util.text import unicode_quote, unicode_unquote, pretty_size … … from trac.web.chrome import add_link, ad 41 40 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor 42 41 from trac.web.href import Href 43 42 from trac.wiki.api import IWikiSyntaxProvider 43 from trac.wiki.formatter import format_to_oneliner 44 44 45 45 46 46 class InvalidAttachment(TracError): … … class AttachmentModule(Component): 431 431 """ 432 432 for change, realm, id, filename, time, descr, author in \ 433 433 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) 436 435 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 event447 448 def event_formatter(self, event, key):449 return None436 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) 450 449 451 450 # IResourceManager methods 452 451 -
trac/ticket/roadmap.py
diff -r 00135419c47f -r f08a0d1b8960 trac/ticket/roadmap.py
a b from genshi.builder import tag 23 23 24 24 from trac import __version__ 25 25 from trac.attachment import AttachmentModule 26 from trac.config import ExtensionOption 26 27 from trac.context import * 27 28 from trac.core import * 28 29 from trac.perm import IPermissionRequestor … … from trac.util.translation import _ 34 35 from trac.util.translation import _ 35 36 from trac.ticket import Milestone, Ticket, TicketSystem 36 37 from trac.ticket.query import Query 37 from trac.timeline.api import ITimelineEventProvider , TimelineEvent38 from trac.timeline.api import ITimelineEventProvider 38 39 from trac.web import IRequestHandler 39 40 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor 40 41 from trac.wiki.api import IWikiSyntaxProvider 41 from trac. config import ExtensionOption42 from trac.wiki.formatter import format_to_html 42 43 43 44 class ITicketGroupStatsProvider(Interface): 44 45 def get_ticket_group_stats(self, ticket_ids): … … class MilestoneModule(Component): 506 507 cursor.execute("SELECT completed,name,description FROM milestone " 507 508 "WHERE completed>=%s AND completed<=%s", 508 509 (to_timestamp(start), to_timestamp(stop))) 509 for ts, name, description in cursor:510 for completed, name, description in cursor: 510 511 milestone = milestone_realm(id=name) 511 512 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? 519 515 520 516 # Attachments 521 517 for event in AttachmentModule(self.env).get_timeline_events( 522 518 req, milestone_realm, start, stop): 523 519 yield event 524 520 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)) 528 530 529 531 # IRequestHandler methods 530 532 -
trac/ticket/web_ui.py
diff -r 00135419c47f -r f08a0d1b8960 trac/ticket/web_ui.py
a b from trac.ticket import Milestone, Ticke 33 33 from trac.ticket import Milestone, Ticket, TicketSystem, ITicketManipulator 34 34 from trac.ticket import ITicketActionController 35 35 from trac.ticket.notification import TicketNotifyEmail 36 from trac.timeline.api import ITimelineEventProvider , TimelineEvent36 from trac.timeline.api import ITimelineEventProvider 37 37 from trac.util import get_reporter_id 38 38 from trac.util.compat import any 39 39 from trac.util.datefmt import to_timestamp, utc … … from trac.web import IRequestHandler 44 44 from trac.web import IRequestHandler 45 45 from trac.web.chrome import add_link, add_script, add_stylesheet, Chrome, \ 46 46 INavigationContributor, ITemplateProvider 47 47 from trac.wiki.formatter import format_to 48 48 49 49 class InvalidTicket(TracError): 50 50 """Exception raised when a ticket fails validation.""" … … class TicketModule(Component): 204 204 'reopened': ('reopenedticket', 'reopened'), 205 205 'closed': ('closedticket', 'closed'), 206 206 'edit': ('editedticket', 'updated')} 207 207 208 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): 215 214 return None 215 resolution = fields.get('resolution') 216 216 info = '' 217 resolution = fields.get('resolution')218 217 if status == 'edit': 219 218 if 'ticket_details' in filters: 220 219 if len(fields) > 0: … … class TicketModule(Component): 231 230 else: 232 231 return None 233 232 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)) 253 236 254 237 # Ticket changes 255 238 db = self.env.get_db_cnx() … … class TicketModule(Component): 267 250 for id,t,author,type,summary,field,oldvalue,newvalue in cursor: 268 251 if not previous_update or (id,t,author) != previous_update[:3]: 269 252 if previous_update: 270 ev = produce (previous_update, status, fields,271 comment, cid)253 ev = produce_event(previous_update, status, fields, 254 comment, cid) 272 255 if ev: 273 256 yield ev 274 257 status, fields, comment, cid = 'edit', {}, '', None … … class TicketModule(Component): 281 264 else: 282 265 fields[field] = newvalue 283 266 if previous_update: 284 ev = produce(previous_update, status, fields, comment, cid) 267 ev = produce_event(previous_update, status, fields, 268 comment, cid) 285 269 if ev: 286 270 yield ev 287 271 … … class TicketModule(Component): 292 276 " FROM ticket WHERE time>=%s AND time<=%s", 293 277 (ts_start, ts_stop)) 294 278 for row in cursor: 295 ev = produce (row, 'new', {}, None, None)279 ev = produce_event(row, 'new', {}, None, None) 296 280 if ev: 297 281 yield ev 298 282 … … class TicketModule(Component): 302 286 req, ticket_realm, start, stop): 303 287 yield event 304 288 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 310 316 311 317 # Internal methods 312 318 -
trac/timeline/api.py
diff -r 00135419c47f -r f08a0d1b8960 trac/timeline/api.py
a b from trac.web.href import Href 25 25 from trac.web.href import Href 26 26 27 27 28 class TimelineEvent(object):29 """Group event related information.30 31 WARNING: this interface is going to be overhauled32 33 The first two properties are set in the constructor:34 35 provider: reference to the event provider36 37 kind: category of the event, will also be used as the CSS class for38 the event's entry in the timeline39 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 event45 - 'header': markup that comes before the main body46 - 'footer': markup that comes after the main body47 48 The next two are set using the `add_wiki` method.49 50 resource: resource context51 wikitext: dictionary of contextual information52 Standard keys include:53 `body` will be interpreted as the main text54 `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 instance61 62 Other properties:63 64 href_fragment: optional fragment that will position to some place65 within the resource page66 67 direct_href: direct link to the event,, if there's no resource associated68 to it69 """70 71 def __init__(self, *args, **kwargs):72 """`TimelineEvent(provider, kind)` creates an event.73 74 `provider` is the Component which provided the event and75 `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 = None85 self.resource = None86 self.href_fragment = ''87 if isinstance(args[0], Component):88 self.provider = args[0]89 self.kind = args[1]90 self.env = self.provider.env91 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 = href101 if title:102 self.markup['title'] = title103 if markup:104 self.markup['header'] = markup105 106 def get_href(self, href=None):107 if self.resource:108 return get_url(self.env, self.resource, href) + self.href_fragment109 else:110 return self.direct_href111 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 = date119 self.author = author120 self.authenticated = authenticated121 self.ipnr = ipnr122 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] = v128 129 def add_wiki(self, resource, **kwargs):130 """Populate the wikitext dictionary."""131 self.resource = resource132 for k, v in kwargs.iteritems():133 if v:134 self.wikitext[k] = v135 136 def dateuid(self):137 return to_timestamp(self.date)138 139 140 141 28 class ITimelineEventProvider(Interface): 142 29 """Extension point interface for adding sources for timed events to the 143 30 timeline. … … class ITimelineEventProvider(Interface): 161 48 The `filters` parameters is a list of the enabled filters, each item 162 49 being the name of the tuples returned by `get_timeline_filters`. 163 50 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. 165 55 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. 169 65 """ 170 66 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. 174 69 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` 177 76 """ 77 78 -
trac/timeline/templates/timeline.html
diff -r 00135419c47f -r f08a0d1b8960 trac/timeline/templates/timeline.html
a b 42 42 <py:for each="event in events" 43 43 py:with="highlight = precision and precisedate and timedelta(0) <= (event.date - precisedate) < precision"> 44 44 <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)} 47 47 <py:if test="event.author">by <span class="author">${format_author(event.author)}</span></py:if> 48 48 </a> 49 49 </dt> 50 <!--! TODO: move the generation of the description back into the components -->51 50 <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)} 62 52 </dd> 63 53 </py:for> 64 54 </dl> -
trac/timeline/templates/timeline.rss
diff -r 00135419c47f -r f08a0d1b8960 trac/timeline/templates/timeline.rss
a b 11 11 <generator>Trac ${trac.version}</generator> 12 12 <image py:if="chrome.logo.src"> 13 13 <title>${project.name}</title> 14 <url>${chrome.logo.src_abs and chrome.logo.src \14 <url>${chrome.logo.src_abs and chrome.logo.src 15 15 or abs_href(chrome.logo.src)}</url> 16 16 <link>${abs_href.timeline()}</link> 17 17 </image> 18 18 19 19 <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> 21 22 ${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> 26 28 <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) 30 30 }</description> 31 <category>$event.kind</category>31 <category>$event.kind</category> 32 32 </item> 33 33 34 34 </channel> -
trac/timeline/web_ui.py
diff -r 00135419c47f -r f08a0d1b8960 trac/timeline/web_ui.py
a b from trac.config import IntOption, BoolO 28 28 from trac.config import IntOption, BoolOption 29 29 from trac.core import * 30 30 from trac.perm import IPermissionRequestor 31 from trac.timeline.api import ITimelineEventProvider , TimelineEvent31 from trac.timeline.api import ITimelineEventProvider 32 32 from trac.util.compat import sorted 33 33 from trac.util.datefmt import format_date, format_datetime, parse_date, \ 34 34 to_timestamp, utc, pretty_timedelta … … class TimelineModule(Component): 122 122 filters = [] 123 123 # check the request or session for enabled filters, or use default 124 124 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', 127 127 lambda f: len(f) == 2 or f[2]): 128 128 if filters: 129 129 break … … class TimelineModule(Component): 147 147 try: 148 148 for event in provider.get_timeline_events(req, start, stop, 149 149 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)) 154 151 except Exception, e: # cope with a failure of that provider 155 152 self._provider_failure(e, req, provider, filters, 156 153 [f[0] for f in available_filters]) 157 154 158 events = sorted(events, key=lambda e: e.date, reverse=True)159 160 155 # prepare sorted global list 156 events = sorted(events, key=lambda e: e['date'], reverse=True) 161 157 if maxrows: 162 data['events'] = events[:maxrows] 163 else: 164 data['events'] = events 158 events = events[:maxrows] 159 160 data['events'] = events 161 165 162 166 163 if format == 'rss': 167 164 # Get the email addresses of all known users … … class TimelineModule(Component): 260 257 261 258 # Internal methods 262 259 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 268 278 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} 274 281 275 282 def _provider_failure(self, exc, req, ep, current_filters, all_filters): 276 283 """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 33 33 from trac.mimeview import Mimeview, is_binary 34 34 from trac.perm import IPermissionRequestor 35 35 from trac.search import ISearchSource, search_to_sql, shorten_result 36 from trac.timeline.api import ITimelineEventProvider , TimelineEvent36 from trac.timeline.api import ITimelineEventProvider 37 37 from trac.util import embedded_numbers, content_disposition 38 38 from trac.util.compat import any, sorted, groupby 39 39 from trac.util.datefmt import pretty_timedelta, utc … … from trac.web.chrome import add_link, ad 47 47 from trac.web.chrome import add_link, add_script, add_stylesheet, \ 48 48 INavigationContributor, Chrome 49 49 from trac.wiki import IWikiSyntaxProvider, WikiParser 50 from trac.wiki.formatter import format_to_html 50 51 51 52 52 53 class IPropertyDiffRenderer(Interface): … … class ChangesetModule(Component): 763 764 show_files = int(show_files) 764 765 else: 765 766 show_files = 0 # disabled 766 wiki_format = self.wiki_format_messages767 long_messages = self.timeline_long_messages768 767 769 768 repos = self.env.get_repository(req.authname) 770 769 … … class ChangesetModule(Component): 775 774 776 775 for _, changesets in groupby(repos.get_changesets(start, stop), 777 776 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), 809 814 tag.span(count, ' ', 810 815 count > 1 and 811 816 (kind == 'copy' and 812 817 'copies' or kind + 's') or kind)) 813 818 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 break822 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 event835 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'] = True841 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)) 842 847 843 848 # IWikiSyntaxProvider methods 844 849 -
trac/wiki/formatter.py
diff -r 00135419c47f -r f08a0d1b8960 trac/wiki/formatter.py
a b def format_to(flavor, context, wikidom, 1019 1019 1020 1020 def format_to_html(context, wikidom, escape_newlines=False): 1021 1021 if not wikidom: 1022 return ''1022 return Markup() 1023 1023 return HtmlFormatter(context, wikidom).generate(escape_newlines) 1024 1024 1025 1025 def format_to_oneliner(context, wikidom, shorten=False): 1026 1026 if not wikidom: 1027 return ''1027 return Markup() 1028 1028 return InlineHtmlFormatter(context, wikidom).generate(shorten) 1029 1029 1030 1030 def extract_link(context, wikidom): 1031 1031 if not wikidom: 1032 return ''1032 return Markup() 1033 1033 return LinkFormatter(context).match(wikidom) 1034 1034 1035 1035 -
trac/wiki/web_ui.py
diff -r 00135419c47f -r f08a0d1b8960 trac/wiki/web_ui.py
a b from trac.mimeview.api import Mimeview, 28 28 from trac.mimeview.api import Mimeview, IContentConverter 29 29 from trac.perm import IPermissionRequestor 30 30 from trac.search import ISearchSource, search_to_sql, shorten_result 31 from trac.timeline.api import ITimelineEventProvider , TimelineEvent31 from trac.timeline.api import ITimelineEventProvider 32 32 from trac.util import get_reporter_id 33 33 from trac.util.datefmt import to_timestamp, utc 34 34 from trac.util.text import shorten_line … … from trac.web.chrome import add_link, ad 38 38 INavigationContributor, ITemplateProvider 39 39 from trac.web import IRequestHandler 40 40 from trac.wiki.api import IWikiPageManipulator, WikiSystem 41 from trac.wiki.formatter import format_to_oneliner 41 42 from trac.wiki.model import WikiPage 42 43 43 44 class InvalidWikiPage(TracError): … … class WikiModule(Component): 521 522 if 'wiki' in filters: 522 523 wiki_realm = Resource('wiki') 523 524 cursor = db.cursor() 524 cursor.execute("SELECT time,name,comment,author, ipnr,version "525 cursor.execute("SELECT time,name,comment,author,version " 525 526 "FROM wiki WHERE time>=%s AND time<=%s", 526 527 (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): 530 531 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)) 543 534 544 535 # Attachments 545 536 for event in AttachmentModule(self.env).get_timeline_events( 546 537 req, wiki_realm, start, stop): 547 538 yield event 548 539 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 551 556 552 557 # ISearchSource methods 553 558
