Flask蓝图模板文件夹

58

我的 Flask 应用程序布局如下:

myapp/
    run.py
    admin/
        __init__.py
        views.py
        pages/
            index.html
    main/
        __init__.py
        views.py
        pages/
            index.html

_init_.py文件是空的。admin/views.py文件的内容如下:

from flask import Blueprint, render_template
admin = Blueprint('admin', __name__, template_folder='pages')

@admin.route('/')
def index():
    return render_template('index.html')

main/views.pyadmin/views.py 相似:

from flask import Blueprint, render_template
main = Blueprint('main', __name__, template_folder='pages')

@main.route('/')
def index():
    return render_template('index.html')

run.py是:

from flask import Flask
from admin.views import admin
from main.views import main

app = Flask(__name__)
app.register_blueprint(admin, url_prefix='/admin')
app.register_blueprint(main, url_prefix='/main')

print app.url_map

app.run()

现在,如果我访问http://127.0.0.1:5000/admin/,它会正确地显示admin/index.html。但是,http://127.0.0.1:5000/main/仍然显示admin/index.html而不是main/index.html。我检查了app.url_map:

<Rule 'admin' (HEAD, OPTIONS, GET) -> admin.index,
<Rule 'main' (HEAD, OPTIONS, GET) -> main.index,

另外,我验证了main/views.py中的index函数按预期被调用。如果我将main/index.html重命名为其他名称,则它可以正常工作。那么,在不重新命名的情况下,如何实现1http://127.0.0.1:5000/main/1显示main/index.html?

7个回答

78
从Flask 0.8开始,蓝图将指定的template_folder添加到app的searchpath中,而不是将每个目录视为单独的实体。这意味着如果您有两个具有相同文件名的模板,则使用在searchpath中找到的第一个模板。这显然令人困惑,并且当前文档说明不足(参见this bug)。It seems你并不是唯一被这种行为困扰的人。
这种行为的设计理由是使蓝图模板可以轻松地从主应用程序的模板中重写,这些模板是Flask模板搜索路径中的第一条线。
有两种选择:
1. 将每个index.html文件重命名为唯一的名称(例如admin.html和main.html)。 2. 在每个模板文件夹中,将每个模板放在蓝图文件夹的子目录中,然后使用该子目录调用模板。例如,您的管理员模板将是yourapp/admin/pages/admin/index.html,然后在蓝图内调用render_template('admin/index.html')

2
遇到了类似的问题。我希望这个问题能够被默认处理得更好。改变静态文件夹的位置可以很好地提供文件服务,但是如果同名文件已经存在,模板就会被覆盖。 - marcin_koss
为什么不直接将 template_folder 更改为 "admin/pages""main/pages" 呢? - Jan Kaifer
@JanKaifer 我也试过同样的事情,包含其他模板是可以的,但是使用extends时,它优先考虑根模板级别,经过6年,有没有摆脱这个问题的机会? - TomSawyer
3
@DarkSuniuM,它仍然存在,但您无法使用相同的名称模板,设计真的很糟糕。 - TomSawyer
7
创建蓝图时,指定模板文件夹的目的是什么?我原以为render_template只会查找那个文件夹。 - variable
显示剩余3条评论

24
除了上面linqq的好建议之外,如果需要的话,您还可以覆盖默认功能。有几种方法:
可以在子类化的Flask应用程序中覆盖create_global_jinja_loader(返回在flask / templating.py中定义的DispatchingJinjaLoader)。这不是推荐的方法,但可以使用。之所以不建议这样做是因为DispatchingJinjaLoader具有足够的灵活性来支持自定义加载程序的注入。如果您自己的加载程序出了问题,它将能够依靠默认的、健全的功能。
因此,推荐的方法是“重写jinja_loader函数”。这就是缺乏文档的地方。修补Flask的加载策略需要一些没有记录的知识,以及对Jinja2的深入了解。
您需要了解两个组件:
- Jinja2环境 - Jinja2模板加载程序

这些都是由Flask自动创建的,具有明智的默认值。(顺便说一下,您可以通过覆盖app.jinja_options来指定自己的Jinja2选项--但请注意,除非您自己指定,否则您将失去Flask默认包含的两个扩展程序--autoescapewith。请查看flask / app.py以了解它们的引用方式。)

环境包含所有这些上下文处理器(例如,您可以在模板中执行var|tojson)、辅助函数(url_for等)和变量(g, session, app)。它还包含对模板加载器的引用,在这种情况下是前面提到的自动实例化的DispatchingJinjaLoader。因此,当您在应用程序中调用render_template时,它会找到或创建Jinja2环境,设置所有这些好东西,并在其上调用get_template,而后者又在DispatchingJinjaLoader内部调用get_source,并尝试几种策略。

如果一切按计划进行,该链将解析查找文件并返回其内容(以及一些其他数据)。此外,请注意这是与{% extend 'foo.htm' %}相同的执行路径。 DispatchingJinjaLoader有两个作用:首先,它检查应用程序的全局加载器app.jinja_loader是否可以定位文件。如果失败,它会检查所有应用程序蓝图(按注册顺序,据我所知)以查找blueprint.jinja_loader来尝试定位文件。追踪该链到最后,这里是jinja_loader的定义(在flask/helpers.py中,_PackageBoundObject,Flask应用程序和Blueprints的基类):
def jinja_loader(self):
    """The Jinja loader for this package bound object.

    .. versionadded:: 0.5
    """
    if self.template_folder is not None:
        return FileSystemLoader(os.path.join(self.root_path,
                                             self.template_folder))

啊!现在我们明白了。显然,两者的命名空间会因相同目录名称而发生冲突。由于全局加载器先被调用,它总是胜出。(FileSystemLoader是几个标准Jinja2加载器之一。)然而,这意味着没有真正简单的方法来重新排序Blueprint和应用程序范围的模板加载器。
因此,我们需要修改DispatchingJinjaLoader的行为。有一段时间,我认为没有好的非强制性和有效的方法来解决这个问题。但是,显然如果我们覆盖app.jinja_options['loader']本身,我们就可以得到想要的行为。因此,如果我们子类化DispatchingJinjaLoader,并修改一个小函数(我认为完全重新实现可能更好,但现在这样也可以),我们就可以得到我们想要的行为。总的来说,一个合理的策略是以下内容(未经测试,但应该适用于现代Flask应用程序):
from flask.templating import DispatchingJinjaLoader
from flask.globals import _request_ctx_stack

class ModifiedLoader(DispatchingJinjaLoader):
    def _iter_loaders(self, template):
        bp = _request_ctx_stack.top.request.blueprint
        if bp is not None and bp in self.app.blueprints:
            loader = self.app.blueprints[bp].jinja_loader
            if loader is not None:
                yield loader, template

        loader = self.app.jinja_loader
        if loader is not None:
            yield loader, template

这个修改了原始加载器的策略,有两种方式:首先尝试从蓝图中加载(仅限当前执行的蓝图,而不是所有蓝图),如果失败,则从应用程序中加载。如果您喜欢所有蓝图的行为,可以从flask/templating.py中复制一些内容。
要将其全部绑定在一起,您需要在Flask对象上设置“jinja_options”:
app = Flask(__name__)
# jinja_options is an ImmutableDict, so we have to do this song and dance
app.jinja_options = Flask.jinja_options.copy() 
app.jinja_options['loader'] = ModifiedLoader(app)

第一次需要模板环境(因此实例化)时,也就是第一次调用render_template时,应使用您的加载器。

这个方法允许模板继承吗? - Andy

16

twooster的答案很有趣,但另一个问题是Jinja默认会根据模板名称缓存模板。因为两个模板都命名为“index.html”,所以加载器不会为后续的蓝图运行。

除了linqq提出的两个建议外,第三个选择是完全忽略蓝图的templates_folder选项,并将模板放置在应用程序模板目录中的各自文件夹中。

即:

myapp/templates/admin/index.html
myapp/templates/main/index.html

2
这个答案(尽管Twoosters解决得很好)在我看来是最有道理的,因为如果你想要实现本地优先效果,那么你就不应该使用蓝图级别的模板来达到它们的预期目的——即从应用特定模板中进行扩展和/或覆盖。简单通常更好(而且在这种情况下,更符合意图的性质)。 - Peter M. Elias
如果我正确理解你的回答,你说 Jinja 在两个模板命名为 index.html 时存在缓存问题,但是你推荐的替代方案仍然有两个命名为 index.html 的模板。我有什么遗漏吗? - Jeff Widman
1
区别似乎在于两个新的index.html文件是通过它们在模板文件夹下的路径进行区分,而不是像“myapp/admin/templates/index.html”和“myapp/main/templates/index.html”那样直接位于它们各自蓝图的模板文件夹下。 - Thinkable
这将限制组织模板文件夹的方式,例如如果我有多个模块,每个文件夹中都有模板。不得不命名不同的模板文件对于Jinja来说是相当糟糕的设计。 - TomSawyer
我不喜欢这个的唯一原因是它破坏了使用蓝图的一个原因。蓝图本应该防止命名冲突,但在这种情况下却产生了一个不应该存在的冲突。 - Inyoka

3

感谢 @linqq,你的方法在这里非常有效,此外我通过使用装饰器提供了更好的解决方案。

注意,不要像这样导入render_template函数:

from flask import render_template

您应该像这样导入flask模块:

import flask

然后,在你的路由器文件顶部添加以下代码块:
def render_decorate(path_prefix):
    def decorate(func):
        def dec_func(*args, **kw):
            arg_list = list(args)
            arg_list[0] = path_prefix + str(arg_list[0])
            arg_tuple = tuple(arg_list)
            return func(*arg_tuple, **kw)
        return dec_func
    return decorate

@render_decorate("%YOUR_DIRECTORY_NAME%/")
def render_template(template_name_or_list, **context):
    return flask.render_template(template_name_or_list, **context)

请将%YOUR_DIRECTORY_NAME%替换为您的实际路径,并确保您的模板文件夹结构如下所示:文件夹结构 完成所有步骤后,只需像往常一样使用render_template函数即可。

0

我在fypress和fybb上使用类似这样的东西,因为我有一个主题系统。

# utils.templates
from jinja2 import Environment, PackageLoader
from flask.templating import _default_template_ctx_processor
from flask import current_app, url_for, get_flashed_messages


admin_env = Environment(
    loader=PackageLoader('fypress', '/templates/admin/'),
    extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'],
    autoescape=True
)

def render_template(template, **kwargs):
    kwargs.update(_default_template_ctx_processor())
    kwargs.update({
        'url_for': url_for,
        'get_flashed_messages': get_flashed_messages # etc...

    })
    kwargs.update(dict(debug=current_app.config.get('DEBUG'), flask_config=current_app.config))

    template = admin_env.get_template(template)
    return template.render(**kwargs)

然后

# routes.admin.
from flask import Blueprint
from utils.templates import render_template

admin_bp = Blueprint('admin', __name__,  url_prefix='/admin')

@admin_bp.route('/')
def root():
    return render_template('index.html', title='Admin')

0
一个简单的解决方法是指定template_folder = "templates",然后在渲染模板时将蓝图名称指定为父目录,如下所示。
@users.route("/")
def users_index():
    return render_template('users/index.html')

请注意,以上解决方案仅在您首先在您的蓝图下的模板文件夹中创建了一个名为“blueprint”的子文件夹时才有效。

0

目前这是对我有效的方法。首先在蓝图模板文件夹中搜索模板,如果找不到则在应用程序模板文件夹中搜索(用于布局等)。

from jinja2 import BaseLoader, TemplateNotFound
from flask import Flask, current_app, request

class BlueprintLoader(BaseLoader):
    def get_source(self, environment, template):
        for loader in (current_app.blueprints[request.blueprint].jinja_loader, current_app.jinja_loader):
            try:
                if loader:
                    return loader.get_source(environment, template)
            except TemplateNotFound:
                pass
        raise TemplateNotFound(template)

app = Flask(__name__)
app.jinja_env.loader = BlueprintLoader()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接