【问题标题】:flask blueprint template folder烧瓶蓝图模板文件夹
【发布时间】:2011-11-02 01:13:09
【问题描述】:

我的烧瓶应用布局是:

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')

ma​​in/views.py 类似于 admin/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 中的索引函数按预期调用。 如果我将 main/index.html 重命名为不同的名称,那么它可以工作。所以,没有 重命名,如何实现 1http://127.0.0.1:5000/main/1 显示 main/index.html?

【问题讨论】:

    标签: python flask


    【解决方案1】:

    从 Flask 0.8 开始,蓝图将指定的 template_folder 添加到应用程序的搜索路径中,而不是将每个目录视为单独的实体。这意味着如果您有两个具有相同文件名的模板,则在搜索路径中找到的第一个就是使用的模板。诚然,这令人困惑,并且目前的记录很差(请参阅this bug)。 It seems 你不是唯一一个对这种行为感到困惑的人。

    这种行为的设计原因是,蓝图模板可以很容易地从主应用程序的模板中覆盖,这些模板是 Flask 模板搜索路径中的第一行。

    我想到了两个选项。

    • 将每个 index.html 文件重命名为唯一的(例如 admin.htmlmain.html)。
    • 在每个模板文件夹中,将每个 蓝图文件夹的子目录中的模板,然后调用 使用该子目录的模板。例如,您的管理模板将是 yourapp/admin/pages/admin/index.html,然后从内部调用 蓝图为render_template('admin/index.html')

    【讨论】:

    • 有类似的问题。我希望这是开箱即用的不同处理方式。更改静态文件夹的位置适用于提供文件,但如果相同的文件已经存在,则模板获取会被覆盖。
    • 正如 jay chan 所指出的,最简单的方法是将模板存储在主模板文件夹中,以便 admin.html 存储在 myapp/templates/admin/index.html
    • 你为什么不把template_folder改成"admin/pages""main/pages"
    • @DarkSuniuM 还在,你不能用同名模板,真是糟糕的设计
    • 创建蓝图时,指定template_folder的目的是什么?我原以为 render_template 只会查看该文件夹
    【解决方案2】:

    除了上面 linqq 的好建议外,如果需要,您还可以覆盖默认功能。有几种方法:

    可以在子类化的 Flask 应用程序中覆盖 create_global_jinja_loader(返回在 flask/template.py 中定义的 DispatchingJinjaLoader)。不建议这样做,但会起作用。不鼓励这样做的原因是DispatchingJinjaLoader 具有足够的灵活性来支持自定义加载程序的注入。如果你把自己的加载器搞砸了,它就能依靠默认的、健全的功能。

    因此,推荐的是“覆盖jinja_loader 函数”。这就是缺乏文档的原因。修补 Flask 的加载策略需要一些似乎没有文档记录的知识,以及对 Jinja2 的良好理解。

    您需要了解两个组件:

    • Jinja2 环境
    • Jinja2 模板加载器

    这些是由 Flask 自动创建的,具有合理的默认值。 (顺便说一下,你可以指定你自己的Jinja2 options,通过覆盖app.jinja_options——但请记住,你会丢失Flask默认包含的两个扩展——autoescapewith——除非你自己指定它们。看看 flask/app.py 看看它们是如何引用这些的。)

    环境包含所有这些上下文处理器(例如,您可以在模板中执行var|tojson)、辅助函数(url_for 等)和变量(gsessionapp) .它还包含对模板加载器的引用,在本例中为上述和自动实例化的DispatchingJinjaLoader。因此,当您在应用程序中调用 render_template 时,它会查找或创建 Jinja2 环境,设置所有这些好东西,并在其上调用 get_template,然后在 DispatchingJinjaLoader 内部调用 get_source,这会尝试后面会介绍一些策略。

    如果一切按计划进行,该链将解决查找文件并返回其内容(以及一些other data)。另请注意,这与{% extend 'foo.htm' %} 采用的执行路径相同。

    DispatchingJinjaLoader 做了两件事:首先它检查应用程序的全局加载器app.jinja_loader 是否可以找到文件。否则,它会检查 所有 应用程序蓝图(按注册顺序,AFAIK)以查找 blueprint.jinja_loader 以尝试定位文件。跟踪该链到最后,这里是 jinja_loader 的定义(在 flask/helpers.py 中,_PackageBoundObject,Flask 应用程序和蓝图的基类):

    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 加载器之一。)然而,这意味着没有真正简单的方法来重新排序蓝图和应用程序范围的模板加载器。

    所以,我们需要修改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/template.py 做一些copy-pasta。

    要将这一切联系在一起,您必须在 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 时,应该使用您的加载器。

    【讨论】:

    • 这太棒了!感谢您的提示!
    • 这个方法是否允许模板继承?
    【解决方案3】:

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

    除了 linqq 的两个建议之外,第三种选择是一起忽略蓝图的 templates_folder 选项,并将模板放在应用程序模板目录中的各个文件夹中。

    即:

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

    【讨论】:

    • 这个答案(尽管 Twoosters 已经很好地解决了)在 IMO 中最有意义,因为如果您试图实现本地优先级效果,那么您就不会使用蓝图级模板来达到预期目的- - 从应用程序特定模板扩展和/或覆盖。越简单通常越好。 (在这种情况下,更符合意图的性质)。
    • 如果我正确理解您的答案,您说 Jinja 在两个模板名为 index.html 时存在缓存问题,但随后推荐一个仍然有两个名为 index.html 的模板的替代方案。我错过了什么吗?
    • 区别似乎是两个新的 index.html 文件是通过它们在模板文件夹下的路径来区分的,而不是直接在它们的蓝图的模板文件夹下,比如 'myapp/admin/templates /index.html' 和 'myapp/main/templates/index.html'。
    • 这会限制组织模板文件夹的方式,例如,如果我在每个文件夹中有多个带有模板的模块。必须以不同的方式命名模板文件对于 jinja 来说是一个非常糟糕的设计
    【解决方案4】:

    Tks @linqq,你的方法在这里真的很好用,除了我通过装饰器做了一个更好的解决方案。

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

    from flask import render_template
    

    你应该像这样导入烧瓶模块:

    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% 替换为您的实际路径,并确保您的模板文件夹如下所示: Folder Structure

    一切都完成了!照常使用 render_template 函数即可。

    【讨论】:

      【解决方案5】:

      我在 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')
      

      【讨论】:

        【解决方案6】:

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

        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()
        

        【讨论】:

          【解决方案7】:

          一个简单的解决方法是指定 template_folder = "templates",然后在渲染模板时,您可以将蓝图名称指定为父目录,如下所示

          @users.route("/")
          def users_index():
              return render_template('users/index.html')
          

          请注意,上述解决方案仅在您首先在模板文件夹中创建了一个具有蓝图名称的子文件夹时才有效,该文件夹将位于您的蓝图下

          【讨论】:

            猜你喜欢
            • 2018-04-17
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-04-22
            • 2012-09-27
            • 1970-01-01
            相关资源
            最近更新 更多