Ticket #2296: content-converter.diff
| File content-converter.diff, 20.5 KB (added by athomas, 3 years ago) |
|---|
-
trac/mimeview/api.py
23 23 24 24 from trac.config import IntOption, Option 25 25 from trac.core import * 26 from trac.util import to_utf8, to_unicode 26 from trac.util import to_utf8, to_unicode, sorted 27 27 from trac.util.markup import escape, Markup, Fragment, html 28 28 29 29 … … 213 213 annotation data.""" 214 214 215 215 216 class IContentConverter(Interface): 217 """An extension point interface for generic MIME based content 218 conversion.""" 219 220 def get_conversions(): 221 """Return an iterable of tuples in the form (key, name, extension, 222 in_mimetype, out_mimetype, quality) representing the MIME conversions 223 supported and 224 the quality ratio of the conversion in the range 0 to 9, where 0 means 225 no support and 9 means "perfect" support. eg. ('latex', 'LaTeX', 'tex', 226 'text/x-trac-wiki', 'text/plain', 8)""" 227 228 def convert_content(req, mimetype, content, key): 229 """Convert the given content from mimetype to the output MIME type 230 represented by key. Returns a tuple in the form (content, 231 output_mime_type).""" 232 233 216 234 class Mimeview(Component): 217 235 """A generic class to prettify data, typically source code.""" 218 236 219 237 renderers = ExtensionPoint(IHTMLPreviewRenderer) 220 238 annotators = ExtensionPoint(IHTMLPreviewAnnotator) 239 converters = ExtensionPoint(IContentConverter) 221 240 222 241 default_charset = Option('trac', 'default_charset', 'iso-8859-15', 223 242 """Charset to be used when in doubt.""") … … 230 249 231 250 # Public API 232 251 252 def get_supported_conversions(self, mimetype): 253 """Return a list of target MIME types in same form as 254 `IContentConverter.get_conversions()`, but with the converter 255 component appended. Output is ordered from best to worst quality.""" 256 converters = [] 257 for converter in self.converters: 258 for k, n, e, im, om, q in converter.get_conversions(): 259 if im == mimetype and q > 0: 260 converters.append((k, n, e, im, om, q, converter)) 261 converters = sorted(converters, key=lambda i: i[-1], reverse=True) 262 return converters 263 264 def convert_content(self, req, mimetype, content, key, filename=None, 265 url=None): 266 """Convert the given content to the target MIME type represented by 267 `key`, which can be either a MIME type or a key. Returns a tuple of 268 (content, output_mime_type, extension).""" 269 if not content: 270 return ('', 'text/plain;charset=utf-8') 271 272 # Ensure we have a MIME type for this content 273 full_mimetype = mimetype 274 if not full_mimetype: 275 if hasattr(content, 'read'): 276 content = content.read(self.get_max_preview_size()) 277 full_mimetype = self.get_mimetype(filename, content) 278 if full_mimetype: 279 mimetype = full_mimetype.split(';')[0].strip() # split off charset 280 else: 281 mimetype = full_mimetype = 'text/plain' # fallback if not binary 282 283 # Choose best converter 284 candidates = self.get_supported_conversions(mimetype) 285 candidates = [c for c in candidates if key in (c[0], c[4])] 286 if not candidates: 287 raise TracError('No available MIME conversions from %s to %s' % 288 (mimetype, key)) 289 290 # First candidate which converts successfully wins. 291 for ck, name, ext, input_mimettype, output_mimetype, quality, \ 292 converter in candidates: 293 try: 294 output = converter.convert_content(req, mimetype, content, ck) 295 if not output: 296 continue 297 return (output[0], output[1], ext) 298 except Exception, e: 299 self.log.warning('MIME conversion using %s failed (%s)' 300 % (converter, e), exc_info=True) 301 raise TracError('No available MIME conversions from %s to %s' % 302 (mimetype, key)) 303 233 304 def get_annotation_types(self): 234 305 """Generator that returns all available annotation types.""" 235 306 for annotator in self.annotators: -
trac/ticket/web_ui.py
17 17 import os 18 18 import re 19 19 import time 20 from StringIO import StringIO 20 21 21 22 from trac.attachment import attachment_to_hdf, Attachment 22 23 from trac.config import BoolOption, Option … … 25 26 from trac.ticket import Milestone, Ticket, TicketSystem, ITicketManipulator 26 27 from trac.ticket.notification import TicketNotifyEmail 27 28 from trac.Timeline import ITimelineEventProvider 28 from trac.util import format_datetime, get_reporter_id, pretty_timedelta 29 from trac.util import format_datetime, get_reporter_id, pretty_timedelta, \ 30 CRLF, http_date 29 31 from trac.util.markup import html, Markup 30 32 from trac.web import IRequestHandler 31 33 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor 32 34 from trac.wiki import wiki_to_html, wiki_to_oneliner 35 from trac.mimeview.api import Mimeview, IContentConverter 33 36 34 37 35 38 class TicketModuleBase(Component): … … 179 182 180 183 class TicketModule(TicketModuleBase): 181 184 182 implements(INavigationContributor, IRequestHandler, ITimelineEventProvider) 185 implements(INavigationContributor, IRequestHandler, ITimelineEventProvider, 186 IContentConverter) 183 187 184 188 default_version = Option('ticket', 'default_version', '', 185 189 """Default version for newly created tickets.""") … … 200 204 """Enable the display of all ticket changes in the timeline 201 205 (''since 0.9'').""") 202 206 207 # IContentConverter methods 208 209 def get_conversions(self): 210 yield ('csv', 'Comma-delimited Text', 'csv', 211 'application/x-trac-ticket', 'text/plain', 9) 212 yield ('tab', 'Tab-delimited Text', 'csv', 'application/x-trac-ticket', 213 'text/plain', 9) 214 yield ('rss', 'RSS Feed', 'xml', 'application/x-trac-ticket', 215 'application/rss+xml', 9) 216 217 def convert_content(self, req, mimetype, ticket, key): 218 if key == 'csv': 219 return self.export_csv(ticket) 220 elif key == 'tab': 221 return self.export_csv(ticket, sep='\t') 222 elif key == 'rss': 223 return self.export_rss(req, ticket) 224 203 225 # INavigationContributor methods 204 226 205 227 def get_active_navigation_item(self, req): … … 255 277 256 278 self._insert_ticket_data(req, db, ticket, reporter_id) 257 279 280 format = req.args.get('format') 281 if format: 282 content, output_type, ext = Mimeview(self.env).convert_content( 283 req, 'application/x-trac-ticket', ticket, 284 format) 285 req.send_response(200) 286 req.send_header('Content-Type', output_type) 287 req.send_header('Content-Disposition', 288 'filename=#%i.%s' % (ticket.id, ext)) 289 req.end_headers() 290 req.write(content) 291 return 292 258 293 # If the ticket is being shown in the context of a query, add 259 294 # links to help navigate in the query result set 260 295 if 'query_tickets' in req.session: … … 274 309 add_link(req, 'up', req.session['query_href']) 275 310 276 311 add_stylesheet(req, 'common/css/ticket.css') 312 313 # Add registered converters 314 for conversion in Mimeview(self.env).get_supported_conversions( 315 'application/x-trac-ticket'): 316 conversion_href = req.href.ticket(ticket.id, format=conversion[0]) 317 add_link(req, 'alternate', conversion_href, conversion[1], 318 conversion[3]) 319 277 320 return 'ticket.cs', None 278 321 279 322 # ITimelineEventProvider methods … … 371 414 372 415 # Internal methods 373 416 417 def export_csv(self, ticket, sep=',', mimetype='text/plain'): 418 content = StringIO() 419 content.write(sep.join(['id'] + [f['name'] for f in ticket.fields]) 420 + CRLF) 421 content.write(sep.join([unicode(ticket.id)] + 422 [ticket.values.get(f['name'], '') 423 .replace(sep, '_').replace('\\', '\\\\') 424 .replace('\n', '\\n').replace('\r', '\\r') 425 for f in ticket.fields]) + CRLF) 426 return (content.getvalue(), '%s;charset=utf-8' % mimetype) 427 428 def export_rss(self, req, ticket): 429 db = self.env.get_db_cnx() 430 changelog = ticket.get_changelog(db=db) 431 curr_author = None 432 curr_date = 0 433 changes = [] 434 change_summary = {} 435 436 description = wiki_to_html(ticket['description'], self.env, req, db) 437 req.hdf['ticket.description.formatted'] = unicode(description) 438 439 def update_title(): 440 if not changes: return 441 title = '; '.join(['%s %s' % (', '.join(v), k) 442 for k, v in change_summary.iteritems()]) 443 changes[-1]['title'] = title 444 445 for date, author, field, old, new in changelog: 446 if date != curr_date or author != curr_author: 447 update_title() 448 change_summary = {} 449 450 changes.append({ 451 'date': http_date(date), 452 'author': author, 453 'fields': {} 454 }) 455 curr_date = date 456 curr_author = author 457 if field == 'comment': 458 change_summary['added'] = ['comment'] 459 changes[-1]['comment'] = unicode(wiki_to_html(new, self.env, 460 req, db, 461 absurls=True)) 462 elif field == 'description': 463 change_summary.setdefault('changed', []).append(field) 464 changes[-1]['fields'][field] = '' 465 else: 466 change = 'changed' 467 if not old: 468 change = 'set' 469 elif not new: 470 change = 'deleted' 471 change_summary.setdefault(change, []).append(field) 472 changes[-1]['fields'][field] = {'old': old, 473 'new': new} 474 update_title() 475 req.hdf['ticket.changes'] = changes 476 return (req.hdf.render('ticket_rss.cs'), 'application/rss+xml') 477 478 374 479 def _do_save(self, req, db, ticket): 375 480 if req.perm.has_permission('TICKET_CHGPROP'): 376 481 # TICKET_CHGPROP gives permission to edit the ticket -
trac/ticket/query.py
29 29 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor 30 30 from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider 31 31 from trac.wiki.macros import WikiMacroBase 32 from trac.mimeview.api import Mimeview, IContentConverter 32 33 33 34 34 class QuerySyntaxError(Exception): 35 35 """Exception raised when a ticket query cannot be parsed from a string.""" 36 36 … … 344 344 345 345 class QueryModule(Component): 346 346 347 implements(IRequestHandler, INavigationContributor, IWikiSyntaxProvider) 347 implements(IRequestHandler, INavigationContributor, IWikiSyntaxProvider, 348 IContentConverter) 348 349 350 # IContentConverter methods 351 def get_conversions(self): 352 yield ('rss', 'RSS Feed', 'xml', 'application/x-trac-query', 353 'application/rss+xml', 9) 354 yield ('csv', 'Comma-delimited Text', 'csv', 355 'application/x-trac-query', 'text/plain', 9) 356 yield ('tab', 'Tab-delimited Text', 'csv', 'application/x-trac-query', 357 'text/plain', 9) 358 359 def convert_content(self, req, mimetype, query, key): 360 if key == 'rss': 361 return self.export_rss(req, query) 362 elif key == 'csv': 363 return self.export_csv(query) 364 elif key == 'tab': 365 return self.export_csv(query, '\t') 366 349 367 # INavigationContributor methods 350 368 351 369 def get_active_navigation_item(self, req): … … 392 410 del req.session[var] 393 411 req.redirect(query.get_href()) 394 412 395 add_link(req, 'alternate', query.get_href(format='rss'), 'RSS Feed', 396 'application/rss+xml', 'rss') 397 add_link(req, 'alternate', query.get_href(format='csv'), 398 'Comma-delimited Text', 'text/plain') 399 add_link(req, 'alternate', query.get_href(format='tab'), 400 'Tab-delimited Text', 'text/plain') 413 # Add registered converters 414 for conversion in Mimeview(self.env).get_supported_conversions( 415 'application/x-trac-query'): 416 add_link(req, 'alternate', query.get_href(format=conversion[0]), 417 conversion[1], conversion[3]) 401 418 402 419 constraints = {} 403 420 for k, v in query.constraints.items(): … … 415 432 req.hdf['query.constraints'] = constraints 416 433 417 434 format = req.args.get('format') 418 if format == 'rss':419 self.display_rss(req, query)420 return 'query_rss.cs', 'application/rss+xml'421 elif format == 'csv':422 self.display_csv(req, query)423 elif format == 'tab':424 self.display_csv(req, query, '\t')425 else:426 self.display_html(req, query)427 return 'query.cs', None435 if format: 436 content, output_type, ext = Mimeview(self.env).convert_content( 437 req, 'application/x-trac-query', query, 438 format) 439 req.send_response(200) 440 req.send_header('Content-Type', output_type) 441 req.send_header('Content-Disposition', 'filename=query.' + ext) 442 req.end_headers() 443 req.write(content) 444 return 428 445 446 self.display_html(req, query) 447 return 'query.cs', None 448 429 449 # Internal methods 430 450 431 451 def _get_constraints(self, req): … … 595 615 self.env.is_component_enabled(ReportModule): 596 616 req.hdf['query.report_href'] = req.href.report() 597 617 598 def display_csv(self, req, query, sep=','): 599 req.send_response(200) 600 req.send_header('Content-Type', 'text/plain;charset=utf-8') 601 req.end_headers() 602 618 def export_csv(self, query, sep=',', mimetype='text/plain'): 619 content = StringIO() 603 620 cols = query.get_columns() 604 req.write(sep.join([col for col in cols]) + CRLF)621 content.write(sep.join([col for col in cols]) + CRLF) 605 622 606 623 results = query.execute(self.env.get_db_cnx()) 607 624 for result in results: 608 req.write(sep.join([unicode(result[col]).replace(sep, '_') 609 .replace('\n', ' ') 610 .replace('\r', ' ') 611 for col in cols]) + CRLF) 625 content.write(sep.join([unicode(result[col]).replace(sep, '_') 626 .replace('\n', ' ') 627 .replace('\r', ' ') 628 for col in cols]) + CRLF) 629 return (content.getvalue(), '%s;charset=utf-8' % mimetype) 612 630 613 def display_rss(self, req, query):631 def export_rss(self, req, query): 614 632 query.verbose = True 615 633 db = self.env.get_db_cnx() 616 634 results = query.execute(db) … … 630 648 groupdesc=query.groupdesc and 1 or None, 631 649 verbose=query.verbose and 1 or None, 632 650 **query.constraints) 651 return (req.hdf.render('query_rss.cs'), 'application/rss+xml') 633 652 634 653 # IWikiSyntaxProvider methods 635 654 -
trac/wiki/web_ui.py
33 33 from trac.wiki.api import IWikiPageManipulator 34 34 from trac.wiki.model import WikiPage 35 35 from trac.wiki.formatter import wiki_to_html, wiki_to_oneliner 36 from trac.mimeview.api import Mimeview, IContentConverter 36 37 37 38 38 39 class WikiModule(Component): 39 40 40 41 implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 41 ITimelineEventProvider, ISearchSource )42 ITimelineEventProvider, ISearchSource, IContentConverter) 42 43 43 44 page_manipulators = ExtensionPoint(IWikiPageManipulator) 44 45 46 # IContentConverter methods 47 def get_conversions(self): 48 yield ('txt', 'Plain Text', 'txt', 'text/x-trac-wiki', 'text/plain', 9) 49 50 def convert_content(self, req, mimetype, content, key): 51 return (content, 'text/plain;charset=utf-8') 52 45 53 # INavigationContributor methods 46 54 47 55 def get_active_navigation_item(self, req): … … 111 119 elif action == 'history': 112 120 self._render_history(req, db, page) 113 121 else: 114 if req.args.get('format') == 'txt': 122 format = req.args.get('format') 123 if format: 124 content, output_type, ext = Mimeview(self.env).convert_content( 125 req, 'text/x-trac-wiki', page.text, 126 format) 115 127 req.send_response(200) 116 req.send_header('Content-Type', 'text/plain;charset=utf-8') 128 req.send_header('Content-Type', output_type) 129 req.send_header('Content-Disposition', 130 'filename=%s.%s' % (page.name, ext)) 117 131 req.end_headers() 118 req.write( page.text)132 req.write(content) 119 133 return 134 120 135 self._render_view(req, db, page) 121 136 122 137 req.hdf['wiki.action'] = action … … 362 377 # Ask web spiders to not index old versions 363 378 req.hdf['html.norobots'] = 1 364 379 365 txt_href = req.href.wiki(page.name, version=version, format='txt') 366 add_link(req, 'alternate', txt_href, 'Plain Text', 'text/plain') 380 # Add registered converters 381 for conversion in Mimeview(self.env).get_supported_conversions( 382 'text/x-trac-wiki'): 383 conversion_href = req.href.wiki(page.name, version=version, 384 format=conversion[0]) 385 add_link(req, 'alternate', conversion_href, conversion[1], 386 conversion[3]) 367 387 368 388 req.hdf['wiki'] = {'page_name': page.name, 'exists': page.exists, 369 389 'version': page.version, 'readonly': page.readonly} -
templates/ticket_rss.cs
1 <?xml version="1.0"?> 2 <!-- RSS generated by Trac v<?cs var:trac.version ?> on <?cs var:trac.time ?> --> 3 <rss version="2.0"> 4 <channel><?cs 5 if:project.name_encoded ?> 6 <title><?cs var:project.name_encoded ?>: Ticket <?cs var:title ?></title><?cs 7 else ?> 8 <title>Ticket <?cs var:title ?></title><?cs 9 /if ?> 10 <link><?cs var:base_host ?><?cs var:ticket.href ?></link> 11 <description><?cs var:ticket.description.formatted ?></description> 12 <language>en-us</language> 13 <generator>Trac v<?cs var:trac.version ?></generator><?cs 14 each:change = ticket.changes ?> 15 <item><?cs 16 if:change.author ?><author><?cs var:change.author ?></author><?cs 17 /if ?> 18 <pubDate><?cs var:change.date ?></pubDate> 19 <title><?cs var:change.title ?></title> 20 <link><?cs var:base_host ?><?cs var:ticket.href ?></link> 21 <description> 22 <?cs if:len(change.fields) ?> 23 <ul><?cs 24 each:field = change.fields ?> 25 <li><strong><?cs name:field ?></strong> <?cs 26 if:!field.old ?>set to <em><?cs 27 var:field.new ?></em><?cs 28 elif:field.new ?>changed from <em><?cs var:field.old 29 ?></em> to <em><?cs 30 var:field.new ?></em>.<?cs 31 else ?>deleted<?cs 32 /if ?></li><?cs 33 /each ?> 34 </ul> 35 <?cs /if ?> 36 <?cs var:change.comment ?> 37 </description> 38 <category>Ticket</category> 39 </item><?cs 40 /each ?> 41 </channel> 42 </rss>
