9

I want to render some static files (*.js in particularly) using Django template variables. I believe this is a common use-case, especially when doing anything AJAX-y; I don't want to hardcode AJAX urls in my .js files any more than I want to hardcode them in my .html files. Buuuut of course I don't want those static files to have to run through the template engine at every client request because this will be slow. I am referring to things like URLs (which do not change after compile/deploy) or static (non-db) model attributes. (I suppose there are use cases where these things might be changed at run-time - this is Python, after all- but I think they are uncommon). For some possible template variables (e.g. model fields), of course the file must be rendered at the time of the client request, but this is not what I'm talking about.

So wouldn't it make sense to render some of my static files through the template engine, for a subset of possible template variables, perhaps at the same time as collectstatic?

As far as I can tell this is not currently the case. To be clear, what I am looking for is a solution to render static files through the template engine at compile/deploy-time so that at "client-request-time" they are in fact plain old static files.

Such an approach would avoid these hacks:

Disclaimers:

  • Yes I know there are template engines out there for javascript (mustache, handlebars, prototype, etc). Why should I add another template engine to the stack when Django already has one? Plus the syntax collides! That seems silly.
  • This looks like it takes a crack at it, but it's complicated and not fully implemented.

So:

  1. Is there a solution out there that I am missing?
  2. If not, is there a way to hook into collectstatic (like a pre-collectstatic hook) that would allow one to render certain static files through the template engine before "collecting" them?

EDIT: No responses yet...is this a really dumb question, and I'm missing something obvious? If so...go ahead and let me know...

Community
  • 1
  • 1
andy
  • 1,399
  • 3
  • 12
  • 32
  • 2
    lol I'm up too late, read it as Django render sacrifice through temple. hahaha. – Chris Gunawardena Mar 24 '14 at 12:03
  • All three answers looked like workable solutions. Each one gets an upvote. The bounty goes to the most comprehensive answer which also had the most upvotes (um, 1, but still) at the time of bounty awarding. Thanks all. – andy Mar 24 '14 at 18:08

4 Answers4

4
  1. There are several frameworks for Django for same purpose: django-pipeline, django-assets, and etc. which integrates different static files processing strategies, with varying degrees of difficulty configuring.
    I use an external tool - Grunt (it requires node.js) - for asset post-processing after collectstatic. It is easier and has a lots of plugins for any purpose (source validation, css/js/images minification, merging, testing and etc.).

  2. It is possible to hook in collectstatic by a custom static files storage with overrided post_process method.

example/settings.py

STATIC_ROOT = 'assets'
STATICFILES_STORAGE = 'example.storage.MyStaticFilesStorage'

example/storage.py

import os
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.files.base import ContentFile
from django.template import Template, Context


class MyStaticFilesStorage(StaticFilesStorage):

    def post_process(self, paths, dry_run=False, **options):
        # don't even dare to process the files if we're in dry run mode
        if dry_run:
            return
        js_template_data = {'foo': 'bar'}  # template variables
        js_template_extension = '.jst'
        js_extension = '.js'
        for original_name, (storage, path) in paths.items():
            processed = False
            saved_name = original_name
            original_path, original_extension = os.path.splitext(original_name)
            if original_extension == js_template_extension:
               with storage.open(path) as original_file:
                  saved_name = original_path + js_extension
                  if hasattr(original_file, 'seek'):
                      original_file.seek(0)
                  original_source = original_file.read()
                  c = Context(js_template_data)
                  saved_source = Template(original_source).render(c)
                  self.delete(saved_name)
                  self.delete(original_name)
                  self._save(saved_name, ContentFile(saved_source))
                  processed = True
            yield original_name, saved_name, processed
Evgeniy Generalov
  • 1,296
  • 14
  • 26
  • +1 Grunt for doing things at build/deploy time. Using `post_process` looks smart; I've used custom management commands that I've called after `collectstatic` (via Grunt). To simplify a bit, you could have Django create a single JS file with all your variables. Namespace them with `myapp.global.urls = {...}` (or whatever). Then import them throughout the rest of your JS. I do the same thing with LESS/SASS—a single file, which is built with Grunt, all other files use those variables, then Grunt handles the CSS compilation. – JCotton Mar 20 '14 at 20:16
  • It looks like each one of these could be adapted to address my use case. Great answer. – andy Mar 24 '14 at 18:06
3

A completely different way to approach the problem would be to ask if you really need to get those URLs in javascript--instead, can the Javascript get the URLs from things like data attributes in your HTML?

In other words, you might have wanted:

homepage.html:

<div id="pop-up-modal">pop me up</div>

homepage.js:

$("#pop-up-modal").click(function {
  $.ajax("{% url 'some-class-name %}") 
  ...
});

When it can often be more straightforward to do something like:

homagepage.html:

<div id="pop-up-modal" data-popurl="{% url 'some-class-name' %}">pop me up</div>

homepage.js:

$("#pop-up-modal").click(function {
  $.ajax($(this).data('popurl')) 
  ...
});
Joel Burton
  • 1,466
  • 9
  • 11
1

I think that django-medusa would suit your needs.

By setting up a renderer and using the disk based backend, generating the static files would be as easy as:

django-admin.py staticsitegen
HAL
  • 2,011
  • 17
  • 10
  • After a quick look this looks like a nice approach. I think the use case of a "semi-static" website aligns very well with my "semi-static" staticfiles. If I understand correctly, I would set a list of paths to something like ["/semistaticviews/*",] and then render the output into something in settings.STATICFILES_DIRS? – andy Mar 24 '14 at 17:54
0

You aren't crazy. I was frustrated by this as well and found myself hacking something together for each new Django project I tackled. I think the reason for the lack of direct solutions is that this is mega-drought bone DRY. Its super easy to just hard code these things and call it day. This and the two most common use cases for this involve generating code in one language from code in another which tends to be viewed as suspect.

I've recently published a new Django package django-render-static that solves this problem generically. It piggy-backs on Django's existing template engine infrastructure. A management command called render_static should be run before collectstatic. This command will look for templates registered in settings (or passed as arguments) and render them to your static file location on disk. They're then available for whatever normal static file packaging pipeline you have configured.

I'm sure there are more use cases, but the two most common I've found are providing a reverse utility in static JavaScript thats equivalent to Django's and auto-translating define-like python structures (i.e. choice fields) into JavaScript. The app provides template tags that do both.

The JavaScript url reversal code is guaranteed to be functionally equivalent to Django's reverse function. I won't bother plopping example code down here because it's been well documented.

bckohan
  • 203
  • 1
  • 6