Changes between Version 46 and Version 47 of TracDev/PortingFromGenshiToJinja
- Timestamp:
- Feb 4, 2017, 1:03:00 PM (7 years ago)
Legend:
- Unmodified
- Added
- Removed
- Modified
-
TracDev/PortingFromGenshiToJinja
v46 v47 229 229 Note 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. 230 230 231 232 === Modifying the content without the `ITemplateStreamFilter` interface #ReplacingITemplateStreamFilter233 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 238 231 ==== Produce the correct content directly instead of relying on post-processing 239 232 240 There were two post-processing steps from which plugin writers did benefit, possibly unknowingly:233 There were two post-processing steps performed by Trac using Genshi stream filters from which plugin writers did benefit, possibly unknowingly: 241 234 1. the addition of the `__FORM_TOKEN` hidden parameter to <form> elements, necessary for successful POST operations 242 235 2. accessibility key enabling/disabling … … 262 255 For 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. 263 256 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 260 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. 261 262 With 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 265 In February 2016, 127/898 plugins (14.1%) on trac-hacks.org made use of `filter_stream()` from the `ITemplateStreamFilter` interface. 267 266 268 267 So this means this specific step of the migration, perhaps the less straightforward, will be of interest for most plugin developers. … … 270 269 Note 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. 271 270 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(!).271 One 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(!). 273 272 274 273 The steps for replacing `filter_stream()` are the following: … … 282 281 We'll discuss the specific example of the ticket [source:cboos.git/tracopt/ticket/deleter.py deleter]. 283 282 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 285 In 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: 285 286 {{{#!diff 286 287 diff --git a/tracopt/ticket/deleter.py b/tracopt/ticket/deleter.py … … 304 305 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 []`). 305 306 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 309 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. 310 311 So we had: 309 312 {{{#!python 310 313 def filter_stream(self, req, method, filename, stream, data): … … 320 323 }}} 321 324 322 325 which becomes now: 323 326 {{{#!python 324 327 def post_process_request(self, req, template, data, content_type): … … 333 336 }}} 334 337 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. 338 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`. 339 340 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. 341 342 ==== Modify the content in the client using JavaScript 343 344 Now the "juicy" part: do in JavaScript what the Transform filter did in Python. 345 346 Well, 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. 342 347 a. **producing the content** 343 348 \\ \\