Ticket #2296: mime-system.diff
| File mime-system.diff, 13.6 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, in_mimetype, 222 out_mimetype, quality) representing the MIME conversions supported and 223 the quality ratio of the conversion in the range 0 to 9, where 0 means 224 no support and 9 means "perfect" support. eg. ('latex', ' LaTeX', 225 'text/x-trac-wiki', 'text/plain', 8)""" 226 227 def convert_content(req, mimetype, content, key, filename=None, url=None): 228 """Convert the given content from mimetype to the output MIME type 229 represented by key. Returns a tuple in the form (content, 230 output_mime_type).""" 231 232 216 233 class Mimeview(Component): 217 234 """A generic class to prettify data, typically source code.""" 218 235 219 236 renderers = ExtensionPoint(IHTMLPreviewRenderer) 220 237 annotators = ExtensionPoint(IHTMLPreviewAnnotator) 238 converters = ExtensionPoint(IContentConverter) 221 239 222 240 default_charset = Option('trac', 'default_charset', 'iso-8859-15', 223 241 """Charset to be used when in doubt.""") … … 230 248 231 249 # Public API 232 250 251 def get_supported_conversions(self, mimetype): 252 """Return a list of target MIME types in same form as 253 `IContentConverter.get_conversions()`, but with the converter 254 component appended. Output is ordered from best to worst quality.""" 255 converters = [] 256 for converter in self.converters: 257 for descriptor in converter.get_conversions(): 258 if descriptor[2] == mimetype and descriptor[4] > 0: 259 converters.append(tuple(descriptor) + (converter,)) 260 converters = sorted(converters, key=lambda i: i[2], reverse=True) 261 return converters 262 263 def convert_content(self, req, mimetype, content, key, filename=None, 264 url=None): 265 """Convert the given content to the target MIME type represented by 266 `key`, which can be either a MIME type or a key. Returns a tuple of 267 (content, output_mime_type).""" 268 if not content: 269 return ('', 'text/plain;charset=utf-8') 270 271 # Ensure we have a MIME type for this content 272 full_mimetype = mimetype 273 if not full_mimetype: 274 if hasattr(content, 'read'): 275 content = content.read(self.get_max_preview_size()) 276 full_mimetype = self.get_mimetype(filename, content) 277 if full_mimetype: 278 mimetype = full_mimetype.split(';')[0].strip() # split off charset 279 else: 280 mimetype = full_mimetype = 'text/plain' # fallback if not binary 281 282 # Choose best converter 283 candidates = self.get_supported_conversions(mimetype) 284 candidates = [c for c in candidates if key in (c[0], c[3])] 285 if not candidates: 286 raise TracError('No available MIME conversions from %s to %s' % 287 (mimetype, key)) 288 289 # First candidate which converts successfully wins. 290 for c_key, name, input_mimettype, output_mimetype, quality, converter in candidates: 291 try: 292 output = converter.convert_content(req, mimetype, content, 293 c_key, filename, url) 294 if not output: 295 continue 296 return output 297 except Exception, e: 298 self.log.warning('MIME conversion using %s failed (%s)' 299 % (converter, e), exc_info=True) 300 raise TracError('No available MIME conversions from %s to %s' % 301 (mimetype, key)) 302 233 303 def get_annotation_types(self): 234 304 """Generator that returns all available annotation types.""" 235 305 for annotator in self.annotators: -
trac/ticket/web_ui.py
30 30 from trac.web import IRequestHandler 31 31 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor 32 32 from trac.wiki import wiki_to_html, wiki_to_oneliner 33 from trac.mimeview.api import Mimeview 33 34 34 35 35 36 class TicketModuleBase(Component): … … 255 256 256 257 self._insert_ticket_data(req, db, ticket, reporter_id) 257 258 259 format = req.args.get('format') 260 if format: 261 content, output_type = Mimeview(self.env).convert_content( 262 req, 'application/x-trac-ticket', ticket, 263 format) 264 req.send_response(200) 265 req.send_header('Content-Type', output_type) 266 req.end_headers() 267 req.write(content) 268 return 269 258 270 # If the ticket is being shown in the context of a query, add 259 271 # links to help navigate in the query result set 260 272 if 'query_tickets' in req.session: … … 274 286 add_link(req, 'up', req.session['query_href']) 275 287 276 288 add_stylesheet(req, 'common/css/ticket.css') 289 290 # Add registered converters 291 for conversion in Mimeview(self.env).get_supported_conversions( 292 'application/x-trac-ticket'): 293 conversion_href = req.href.ticket(ticket.id, format=conversion[0]) 294 add_link(req, 'alternate', conversion_href, conversion[1], 295 conversion[3]) 296 277 297 return 'ticket.cs', None 278 298 279 299 # ITimelineEventProvider methods -
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', 'application/x-trac-query', 353 'application/rss+xml', 9) 354 yield ('csv', 'Comma-delimited Text', 'application/x-trac-query', 355 'text/plain', 9) 356 yield ('tab', 'Tab-delimited Text', 'application/x-trac-query', 357 'text/plain', 9) 358 359 def convert_content(self, req, mimetype, query, key, filename=None, url=None): 360 if key == 'rss': 361 return self.display_rss(req, query) 362 elif key == 'csv': 363 return self.display_csv(query) 364 elif key == 'tab': 365 return self.display_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', None 435 if format: 436 content, output_type = Mimeview(self.env).convert_content(req, 437 'application/x-trac-query', query, format) 438 req.send_response(200) 439 req.send_header('Content-Type', output_type) 440 req.end_headers() 441 req.write(content) 442 return 428 443 444 self.display_html(req, query) 445 return 'query.cs', None 446 429 447 # Internal methods 430 448 431 449 def _get_constraints(self, req): … … 595 613 self.env.is_component_enabled(ReportModule): 596 614 req.hdf['query.report_href'] = req.href.report() 597 615 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 616 def display_csv(self, query, sep=','): 617 content = StringIO() 603 618 cols = query.get_columns() 604 req.write(sep.join([col for col in cols]) + CRLF)619 content.write(sep.join([col for col in cols]) + CRLF) 605 620 606 621 results = query.execute(self.env.get_db_cnx()) 607 622 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) 623 content.write(sep.join([unicode(result[col]).replace(sep, '_') 624 .replace('\n', ' ') 625 .replace('\r', ' ') 626 for col in cols]) + CRLF) 627 return (content.getvalue(), 'text/plain;charset=utf-8') 612 628 613 629 def display_rss(self, req, query): 614 630 query.verbose = True … … 630 646 groupdesc=query.groupdesc and 1 or None, 631 647 verbose=query.verbose and 1 or None, 632 648 **query.constraints) 649 return (req.hdf.render('query_rss.cs'), 'application/rss+xml') 633 650 634 651 # IWikiSyntaxProvider methods 635 652 -
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', 'text/x-trac-wiki', 'text/plain', 9) 49 50 def convert_content(self, req, mimetype, content, key, filename=None, 51 url=None): 52 return (content, 'text/plain;charset=utf-8') 53 45 54 # INavigationContributor methods 46 55 47 56 def get_active_navigation_item(self, req): … … 111 120 elif action == 'history': 112 121 self._render_history(req, db, page) 113 122 else: 114 if req.args.get('format') == 'txt': 123 format = req.args.get('format') 124 if format: 125 content, output_type = Mimeview(self.env).convert_content(req, 126 'text/x-trac-wiki', page.text, 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) 117 129 req.end_headers() 118 req.write( page.text)130 req.write(content) 119 131 return 132 120 133 self._render_view(req, db, page) 121 134 122 135 req.hdf['wiki.action'] = action … … 362 375 # Ask web spiders to not index old versions 363 376 req.hdf['html.norobots'] = 1 364 377 365 txt_href = req.href.wiki(page.name, version=version, format='txt') 366 add_link(req, 'alternate', txt_href, 'Plain Text', 'text/plain') 378 # Add registered converters 379 for conversion in Mimeview(self.env).get_supported_conversions( 380 'text/x-trac-wiki'): 381 conversion_href = req.href.wiki(page.name, version=version, 382 format=conversion[0]) 383 add_link(req, 'alternate', conversion_href, conversion[1], 384 conversion[3]) 367 385 368 386 req.hdf['wiki'] = {'page_name': page.name, 'exists': page.exists, 369 387 'version': page.version, 'readonly': page.readonly}
