Edgewall Software

Ticket #2296: mime-system.diff

File mime-system.diff, 13.6 KB (added by athomas, 3 years ago)

Migrated ticket, wiki and query interfaces

  • trac/mimeview/api.py

     
    2323 
    2424from trac.config import IntOption, Option 
    2525from trac.core import * 
    26 from trac.util import to_utf8, to_unicode 
     26from trac.util import to_utf8, to_unicode, sorted 
    2727from trac.util.markup import escape, Markup, Fragment, html 
    2828 
    2929 
     
    213213        annotation data.""" 
    214214 
    215215 
     216class 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 
    216233class Mimeview(Component): 
    217234    """A generic class to prettify data, typically source code.""" 
    218235 
    219236    renderers = ExtensionPoint(IHTMLPreviewRenderer) 
    220237    annotators = ExtensionPoint(IHTMLPreviewAnnotator) 
     238    converters = ExtensionPoint(IContentConverter) 
    221239 
    222240    default_charset = Option('trac', 'default_charset', 'iso-8859-15', 
    223241        """Charset to be used when in doubt.""") 
     
    230248 
    231249    # Public API 
    232250 
     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 
    233303    def get_annotation_types(self): 
    234304        """Generator that returns all available annotation types.""" 
    235305        for annotator in self.annotators: 
  • trac/ticket/web_ui.py

     
    3030from trac.web import IRequestHandler 
    3131from trac.web.chrome import add_link, add_stylesheet, INavigationContributor 
    3232from trac.wiki import wiki_to_html, wiki_to_oneliner 
     33from trac.mimeview.api import Mimeview 
    3334 
    3435 
    3536class TicketModuleBase(Component): 
     
    255256 
    256257        self._insert_ticket_data(req, db, ticket, reporter_id) 
    257258 
     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 
    258270        # If the ticket is being shown in the context of a query, add 
    259271        # links to help navigate in the query result set 
    260272        if 'query_tickets' in req.session: 
     
    274286                add_link(req, 'up', req.session['query_href']) 
    275287 
    276288        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 
    277297        return 'ticket.cs', None 
    278298 
    279299    # ITimelineEventProvider methods 
  • trac/ticket/query.py

     
    2929from trac.web.chrome import add_link, add_stylesheet, INavigationContributor 
    3030from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider 
    3131from trac.wiki.macros import WikiMacroBase 
     32from trac.mimeview.api import Mimeview, IContentConverter 
    3233 
    33  
    3434class QuerySyntaxError(Exception): 
    3535    """Exception raised when a ticket query cannot be parsed from a string.""" 
    3636 
     
    344344 
    345345class QueryModule(Component): 
    346346 
    347     implements(IRequestHandler, INavigationContributor, IWikiSyntaxProvider) 
     347    implements(IRequestHandler, INavigationContributor, IWikiSyntaxProvider, 
     348               IContentConverter) 
    348349 
     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 
    349367    # INavigationContributor methods 
    350368 
    351369    def get_active_navigation_item(self, req): 
     
    392410                    del req.session[var] 
    393411            req.redirect(query.get_href()) 
    394412 
    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]) 
    401418 
    402419        constraints = {} 
    403420        for k, v in query.constraints.items(): 
     
    415432        req.hdf['query.constraints'] = constraints 
    416433 
    417434        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 
    428443 
     444        self.display_html(req, query) 
     445        return 'query.cs', None 
     446 
    429447    # Internal methods 
    430448 
    431449    def _get_constraints(self, req): 
     
    595613           self.env.is_component_enabled(ReportModule): 
    596614            req.hdf['query.report_href'] = req.href.report() 
    597615 
    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() 
    603618        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) 
    605620 
    606621        results = query.execute(self.env.get_db_cnx()) 
    607622        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') 
    612628 
    613629    def display_rss(self, req, query): 
    614630        query.verbose = True 
     
    630646                groupdesc=query.groupdesc and 1 or None, 
    631647                verbose=query.verbose and 1 or None, 
    632648                **query.constraints) 
     649        return (req.hdf.render('query_rss.cs'), 'application/rss+xml') 
    633650 
    634651    # IWikiSyntaxProvider methods 
    635652     
  • trac/wiki/web_ui.py

     
    3333from trac.wiki.api import IWikiPageManipulator 
    3434from trac.wiki.model import WikiPage 
    3535from trac.wiki.formatter import wiki_to_html, wiki_to_oneliner 
     36from trac.mimeview.api import Mimeview, IContentConverter 
    3637 
    3738 
    3839class WikiModule(Component): 
    3940 
    4041    implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 
    41                ITimelineEventProvider, ISearchSource) 
     42               ITimelineEventProvider, ISearchSource, IContentConverter) 
    4243 
    4344    page_manipulators = ExtensionPoint(IWikiPageManipulator) 
    4445 
     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 
    4554    # INavigationContributor methods 
    4655 
    4756    def get_active_navigation_item(self, req): 
     
    111120        elif action == 'history': 
    112121            self._render_history(req, db, page) 
    113122        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) 
    115127                req.send_response(200) 
    116                 req.send_header('Content-Type', 'text/plain;charset=utf-8') 
     128                req.send_header('Content-Type', output_type) 
    117129                req.end_headers() 
    118                 req.write(page.text) 
     130                req.write(content) 
    119131                return 
     132 
    120133            self._render_view(req, db, page) 
    121134 
    122135        req.hdf['wiki.action'] = action 
     
    362375            # Ask web spiders to not index old versions 
    363376            req.hdf['html.norobots'] = 1 
    364377 
    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]) 
    367385 
    368386        req.hdf['wiki'] = {'page_name': page.name, 'exists': page.exists, 
    369387                           'version': page.version, 'readonly': page.readonly}