2

I'm having a rather strange problem using a jinja2.ChoiceLoader (also tried with multiple paths with FileSystemLoader, no joy) in Flask.

I have several "theme" directories, like so.

/templates/
  themes/
    default/
      layout.html
      menu.html
    blue/
      layout.html
    grey/
      menu.html
    ...

And I'd like to fallback to default/ if the selected theme doesn't have the required template, so I used a ChoiceLoader, like so.

@app.before_request
def setup_request():
    current_theme = get_theme()
    logging.info('Using theme %s'%(current_theme))
    app.jinja_loader = jinja2.ChoiceLoader([
        jinja2.FileSystemLoader('/templates/themes/%s/'%(current_theme)),
        jinja2.FileSystemLoader('/templates/themes/default/')
    ])

That's great, but if I change the <current_theme> it still loads the theme from the old folder until I reload Apache or restart the Flask development server.

It should be using the new theme. Logging says that it's using the changed theme, but apparently app.jinja_loader is a bit like honey badger... it's completely ignoring it until I reload Apache.

Edit: This appears to be related to Flask considering all files of the same name to be the same file. I can reproduce with the builtin server (with DEBUG=True), Cherry, and mod_wsgi. This person seems to have a similar problem, but no simple solution: flask blueprint template folder My situation is different in the sense that I require cascading templates for a single app. His problem is related to cascading templates between blueprints, but it may be the same issue under the hood.

Here's the code that's in the "get_theme()" call:

def get_theme():
    # I know this is unsafe, testing only
    return request.args.get('theme','default')

Edit 2: I need to change the HTML and JavaScript between themes, not just the CSS. This is why I'm not just loading different CSS files. Also, some of these themes are for mobile devices, and have very little in common with the other themes.

Edit 3: Two solutions. Solution 1: Name the files uniquely, like "blue.layout.html" and "default.layout.html". This works perfectly, but it doesn't cascade as required. Solution 2: Use relative paths, so instead of include 'file.html', use include 'theme/blue/file.html. I achieved cascading by creating a get_theme_file() function that checks for the active theme, checks if the file exists (if not, fallback to "default" theme), and returns the relative path. I just have to make sure everything I include looks like {% include get_theme_file('file.html') %}. This is not elegant, but I find it to be more elegant that the low-level fiddling with Flask used here.

Community
  • 1
  • 1
Aaron Meier
  • 929
  • 9
  • 21

2 Answers2

0

By the way, you can pass multiple locations to FileSystemLoader, and it is the recommended way to load templates

This is expected behavior in Apache with mod_wsgi (which I assume you are using). File system changes don't trigger a reload of all the processes. See this entry in the Flask docs that talks about this, and offers a workaround which is to add:

WSGIScriptReloading On

To the configuration section for your application and then touching the wsgi file to trigger a reload of the child processes.

Are you sure this is what you intend? Most theme switching tricks rely on the cascading part of cascading style sheets (CSS) to control themes.

Burhan Khalid
  • 169,990
  • 18
  • 245
  • 284
  • I need to control layout, I'll edit my question to reflect that. This is certainly not mod_wsgi behavior, as everything is dynamic, it's specific to flask referencing the same bytecode for different files (if they have the same name), see: [blueprint-template-folder](http://stackoverflow.com/questions/7974771/flask-blueprint-template-folder). Thanks for the FileSystemLoader info, though. – Aaron Meier Apr 14 '13 at 08:46
0

Well, I'm not the only one to encounter this problem. The issue is that Flask caches based on filename, and if you don't include the relative path it just caches the first one to be loaded. There are three ways to accomplish dynamic, cascading, templates.

  1. Override Jinja builtins. Which I found to be quite confusing. I'm not smart enough for this solution.
  2. Serve a different WSGI process for each file. The setup here seems a bit too much for a dynamic site. Flask caches the filenames per WSGI process, so you can do something with multiple Cherry WSGI servers, for example.
  3. Include the theme in the load path. Create a function, load it in with context_processor, and only load template files using that function. The function needs to check for a non-default theme, check if the file exists, and return it. So a template call would be get_theme_file('layout.html') which would return the relative path (like themes/blue/layout.html).

An example of Option 3.

def get_theme_file(fname):
    theme = get_theme()
    if os.path.exists(os.path.join(theme.theme_dir, fname)):
        return os.path.join('themes', theme.name, fname)
    return os.path.join('themes', 'default', fname)
...
    # Each render_template should reference this
    return render_template(get_theme_file('layout.html'))

If you include theme files in templates:

{% include get_theme_file('layout.html') %}

Unfortunately, this doesn't cache, but I can see a few ways to optimize it. Maybe cache an os.listdir of the get_theme().theme_dir, and instead of reading the filesystem for each get_theme_file call, just do an in check against the cached listdir list.

It's worth noting that this is Flask specific. I was unable to reproduce this behavior with plain Jinja2 and my own WSGI server. One might say that Flask was a poor choice for this particular project, but I'd argue that the savings from everything else Flask does was well worth it.

Community
  • 1
  • 1
Aaron Meier
  • 929
  • 9
  • 21