Edgewall Software

Changes between Version 46 and Version 47 of TracDev/PortingFromGenshiToJinja


Ignore:
Timestamp:
Feb 4, 2017, 1:03:00 PM (7 years ago)
Author:
Christian Boos
Comment:

restructure #ReplacingITemplateStreamFilter

Legend:

Unmodified
Added
Removed
Modified
  • TracDev/PortingFromGenshiToJinja

    v46 v47  
    229229Note that in a similar way to `Markup`, `escape` now also comes from `markupsafe`, with some slight adaptations, as `markupsafe.escape` always escapes the quotes, which is something we don't do by default. Hence always import `escape` from `trac.util.html`, never directly from `markupsafe` or Jinja2, unless you really know what you're doing.
    230230
    231 
    232 === Modifying the content without the `ITemplateStreamFilter` interface #ReplacingITemplateStreamFilter
    233 
    234 One of the strengths of Genshi was its ability to transform the normal HTML content and, for example, to inject arbitrary content at any point in the HTML, thanks to the use of the `Transform` stream filter and its API. However, as elegant as it was, this feature was the main performance killer of Genshi, and Jinja2 doesn't propose an equivalent, for good reasons.
    235 
    236 With Jinja2, the content is produced in one step, with no kind of post-processing. Hence the content should either be produced right away, or if it really has to be produced as an extra step, it should be produced dynamically on client-side using JavaScript.
    237 
    238231==== Produce the correct content directly instead of relying on post-processing
    239232
    240 There were two post-processing steps from which plugin writers did benefit, possibly unknowingly:
     233There were two post-processing steps performed by Trac using Genshi stream filters from which plugin writers did benefit, possibly unknowingly:
    241234 1. the addition of the `__FORM_TOKEN` hidden parameter to <form> elements, necessary for successful POST operations
    242235 2. accessibility key enabling/disabling
     
    262255For the accessibility key, it's also quite simple: instead of hard-coding the key as an `accesskey="e"` attribute, simply use the `accesskey('e')` function call, it will know if it has to produce the attribute or not depending on the current user preferences.
    263256
    264 ==== Modify the content in the client using JavaScript
    265 
    266 On this day,  127/898 plugins (14.1%) on trac-hacks.org make use of `filter_stream()` from the `ITemplateStreamFilter` interface.
     257
     258=== Replacing the `ITemplateStreamFilter` interface #ReplacingITemplateStreamFilter
     259
     260One of the strengths of Genshi was its ability to transform the normal HTML content and, for example, to inject arbitrary content at any point in the HTML, thanks to the use of the `Transform` stream filter and its API. However, as elegant as it was, this feature was the main performance killer of Genshi, and Jinja2 doesn't propose an equivalent, for good reasons.
     261
     262With Jinja2, the content is produced in one step, with no possibility of post-processing. The only way left to alter the generated content is to **perform these modifications dynamically on client-side using JavaScript**.
     263
     264
     265In February 2016, 127/898 plugins (14.1%) on trac-hacks.org made use of `filter_stream()` from the `ITemplateStreamFilter` interface.
    267266
    268267So this means this specific step of the migration, perhaps the less straightforward, will be of interest for most plugin developers.
     
    270269Note that though we guarantee some level of support for the `ITemplateStreamFilter` during the transition period, the new suggested way also works great with earlier versions of Trac (1.0 and 1.2, perhaps even 0.12), so there's really no reason to maintain both versions once you did the switch.
    271270
    272 One strong incentive for dropping the `ITemplateStreamFilter` usage in your code is that by not doing so you basically **kill all the performance benefits** of the switch to Jinja2. The support of `ITemplateStreamFilter` implies that we first render the page to HTML using Jinja2, then parse it back as an HTML stream and feed this stream to the Genshi filter, so that it can be transformed, and then finally rendered again(!).
     271One strong incentive for dropping the `ITemplateStreamFilter` usage in your code is that **by not doing so you kill all the performance benefits** brought by the switch to Jinja2. The support of `ITemplateStreamFilter` implies that we first render the page to HTML using Jinja2, then parse it back as an HTML stream and feed this stream to the Genshi filter, so that it can be transformed by these filters, and then finally rendered again(!).
    273272
    274273The steps for replacing `filter_stream()` are the following:
     
    282281We'll discuss the specific example of the ticket [source:cboos.git/tracopt/ticket/deleter.py deleter].
    283282
    284 1. this component already implemented `ITemplateProvider` (for providing the `ticket_delete.html` template), but the `get_htdocs_dir` didn't yet return a location. We now have to return the local `htdocs` directory, as we'll put our JavaScript file there:
     283==== Implement `ITemplateProvider.get_htdocs_dirs` to be able to provide extra JavaScript code
     284
     285In our example, the component already implemented `ITemplateProvider` (for providing the `ticket_delete.html` template), but the `get_htdocs_dir` didn't yet return a location. We now have to return the local `htdocs` directory, as we'll put our JavaScript file there:
    285286  {{{#!diff
    286287diff --git a/tracopt/ticket/deleter.py b/tracopt/ticket/deleter.py
     
    304305  Adding an `ITemplateProvider` implementation from scratch is not more complicated (if there are no templates provided by the plugin, `get_template_dirs()` can simply `return []`).
    305306
    306 2. we need to transfer the logic at the beginning of `filter_stream()` into `post_process_request()`, i.e. the condition for which we decided to either let the content pass through unmodified or to modify it, now becomes the condition for which we decide to either add or not add our extra bit of JavaScript code.
    307 
    308   So we had:
     307==== Implement `IRequestFilter.post_process_request` to conditionally add JavaScript code
     308
     309We need to transfer the logic at the beginning of `filter_stream()` into `post_process_request()`, i.e. the condition for which we decided to either let the content pass through unmodified or to modify it, now becomes the condition for which we decide to either add or not add our extra bit of JavaScript code.
     310
     311So we had:
    309312  {{{#!python
    310313      def filter_stream(self, req, method, filename, stream, data):             
     
    320323  }}}
    321324
    322   which becomes now:
     325which becomes now:
    323326  {{{#!python
    324327      def post_process_request(self, req, template, data, content_type):
     
    333336  }}}
    334337
    335   i.e. the condition remains the same: //the `filename`(/`template`) is either "ticket.html" or "ticket_preview.html", and we have a ticket in the `data`, that ticket exists and we have admin perm on that ticket//; if true, we would have altered the stream in `filter_stream()`, now in `post_process_request()` we'll call `add_script`.
    336 
    337   Note that we also call `add_script_data`. Here we do it for some piece of session information which is not yet available in the default JavaScript data, but you'll probably have to do that for any piece of the template `data` you'll need to use in the JavaScript code. Don't pass the whole `data` dictionary though, that would be overkill and it's quite likely some bits won't convert readily to JSON. Pass only the information you'll need.
    338 
    339  3. now the "juicy" part: do in JavaScript what the Transform filter did in Python.
    340 
    341  Well, actually the //browser// needs JavaScript, but you can use whatever you want in order to produce that JavaScript code. I personally recommend using CoffeeScript as it's well suited for producing the HTML snippets we'll need.
     338i.e. the condition remains the same: //the `filename`(/`template`) is either "ticket.html" or "ticket_preview.html", and we have a ticket in the `data`, that ticket exists and we have admin perm on that ticket//; if true, we would have altered the stream in `filter_stream()`, now in `post_process_request()` we'll call `add_script`.
     339
     340Note that we also call `add_script_data`. Here we do it for some piece of session information which is not yet available in the default JavaScript data, but you'll probably have to do that for any piece of the template `data` you'll need to use in the JavaScript code. Don't pass the whole `data` dictionary though, that would be overkill and it's quite likely some bits won't convert readily to JSON. Pass only the information you'll need.
     341
     342==== Modify the content in the client using JavaScript
     343
     344Now the "juicy" part: do in JavaScript what the Transform filter did in Python.
     345
     346Well, actually the //browser// needs JavaScript, but you can use whatever you want in order to produce that JavaScript code. One possibility is to use CoffeeScript as it's well suited for producing the HTML snippets we'll need.
    342347   a. **producing the content**
    343348      \\ \\