问题 TemplateDoesNotExist在python app-engine django 1.2上,同时模板渲染相对路径


我在Windows机器上本地运行1.4.2 appengine SDK。我有一个运行Django 0.96的应用程序。模板渲染使用的是django包装器

google.appengine.ext.webapp.template.render

渲染模板。我经常使用相对路径链接我的模板,例如

{% extends "../templates/base.html" %}

从...升级到Django 1.2之后的find_template方法 当使用相对路径时,appengine的Django 1.2 lib文件夹中的django.template.loader现在引发TemplateDoesNotExist

for loader in template_source_loaders:
    try:
        #raises TemplateDoesNotExist name='../templates/home.html' dirs=None
        source, display_name = loader(name, dirs)
        return (source, make_origin(display_name, loader, name, dirs))
    except TemplateDoesNotExist:
        pass
raise TemplateDoesNotExist(name)

我一直在逐步浏览Django和AppEngine代码,但看不出任何理由。任何人都可以提供更多见解吗?

谢谢,

理查德


11522
2018-03-10 17:46


起源



答案:


当我从0.96转换为1.2 Django模板时,这个问题也让我感到困惑。当SDK 1.4.2开始发出我需要选择版本的警告时,我最初被推动这样做,但当我研究模板语言中急需的改进时,我急于做出改变。

然后一切都破了。和你一样,我在我身上使用了很多相对路径 extends 和 include 命令。它花了很多调试和挖掘,但我确实找出了问题的原因和一个非常好的解决方案。

原因:在Django 1.2中,加载模板文件的代码使用一个名为的命令启动 safe_join 加入路径部分(你可以看到代码) google_appengine\lib\django_1_2\django\template\loaders\filesystem.py)。它不允许相对路径超出它认为的顶级目录。这与配置为阻止您访问服务器的整个文件系统的Web服务器相同 ..进入你的网址。最终的结果是

{% extends "../templates/base.html" %}

曾经很好的打破规则,它不会起作用。

我在我的应用程序中修复此问题而不完全重构我的模板布局的方法是实现自定义 TemplateLoader。 Django的模板渲染引擎允许应用程序拥有许多不同的类,这些类知道如何以不同的方式查找模板。如果你查看我上面给出的目录,你会看到提供了几个,它们都是继承自的类 BaseLoader。我提供了自己的,我的模板是如何定制的。

我的项目有类似Rails的布局:

app/
   controllers/
      home_controller.py
      posts_controller.py
   models/
      ...
   views/
      home/
          index.html
          about.html
      posts/
          show.html
          new.html
      shared/
          base.html
          post.html

每个模板都扩展 base.html 还有一对包括 post.html,他们以前使用相对路径到达他们的位置 base/。理想情况下,我甚至不想使用 .. up-dir到达那里,但需要0.96。我创建了以下模板加载器来使用我的方案:

from django.conf import settings
from django.template import TemplateDoesNotExist
from django.template.loader import BaseLoader
from django.utils._os import safe_join
import os

class MvcTemplateLoader(BaseLoader):
    "A custom template loader for the MVCEngine framework."

    is_usable = True

    __view_paths = None

    def __init__(self, views_path):
        self.views_path = views_path
        # We only need to instantiate the view_paths class variable once.
        if MvcTemplateLoader.__view_paths is None:
            temp_paths = []
            for each_path in os.listdir(views_path):
                # We want to skip hidden directories, so avoid anything that starts with .
                # This works on both Windows and *NIX, but could it fail for other OS's?
                if not each_path.startswith('.'):
                    full_path = os.path.join(views_path, each_path)
                    if each_path == "shared":
                        # The shared directory is special. Since templates in many other directories will be
                        # inheriting from or including templates there, it should come second, right after the
                        # root views directory. For now, it will be first.
                        temp_paths.insert(0, full_path)
                    else:
                        temp_paths.append(full_path)
            # The root views_path itself will always be first in order to give resolution precendence to templates
            # that are specified with a parent directory. In other words, home/index.html will be immediately
            # resolved with no ambiguity; whereas, index.html could resolve as bar/index.html rather than
            # foo/index.html.
            temp_paths.insert(0, views_path)
            MvcTemplateLoader.__view_paths = temp_paths


    def get_template_sources(self, template_name):
        for template_dir in MvcTemplateLoader.__view_paths:
            try:
                yield safe_join(template_dir, template_name)
            except UnicodeDecodeError:
                # The template dir name was a bytestring that wasn't valid UTF-8.
                raise
            except ValueError:
                # The joined path was located outside of this particular
                # template_dir (it might be inside another one, so this isn't
                # fatal).
                pass

    def load_template_source(self, template_name, template_dirs=None):
        tried = []
        for filepath in self.get_template_sources(template_name):
            try:
                file = open(filepath)
                try:
                    return (file.read().decode(settings.FILE_CHARSET), filepath)
                finally:
                    file.close()
            except IOError:
                tried.append(filepath)

        error_msg = "Could not find %s in any of the views subdirectories." % template_name
        raise TemplateDoesNotExist(error_msg)
    load_template_source.is_usable = True

_loader = MvcTemplateLoader

我使我的自定义模板加载器包含在Django通过更改我的应用程序尝试的集合中 main 功能看起来像这样:

def main():    
    from google.appengine.dist import use_library
    use_library('django', '1.2')

    os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'

    from django.conf import settings 
    views_path = os.path.join(os.path.dirname(__file__), 'app','views')
    settings.TEMPLATE_LOADERS = (('gaemvclib.mvctemplateloader.MvcTemplateLoader', views_path), 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader')

(然后通常进入你的主要功能的所有其他东西)。

所以,我认为您应该能够修改上面的TemplateLoader代码以匹配模板目录的布局,并且它不仅可以让您更好地控制模板层次结构的布局,还可以更好地控制 extends 和 include 声明。你不再使用了 .. 而只是给出模板的路径相对于项目中的任何东西相当于我的 views 目录。


15
2018-03-10 20:57



谢谢。非常感谢。 - probably at the beach
运用 self.response.out.write(template.render(path, template_values)) 使用MvcTemplateLoader类给出了这个错误“ValueError:需要多于1个值才能解压”为什么会这样? - zakdances
Zak,所有这些技术可能已经被1.5.5版本中最近的更改所取代。我没有机会针对这些变化测试我自己的应用程序 - 我已经破坏了AppEngine Go runtine - 但发行说明告诉我渲染区域已经发生了很大的变化。我会调查一下,尽快回复你。 - Adam Crossland
@Adam Crossland我已经使用了你的方法,现在尝试将我的代码移植到python2.7,除了用于渲染模板的正确路径之外,一切都已设置。组织各种深度的文件至关重要,至少对我来说要保持井井有条。我期待你的更新:) - topless
@ Chris-Top,你能告诉我你在获得正确途径时遇到了什么问题吗?我最近一直在使用AppEngine的Go运行时的项目,所以我甚至没想过将我的Python东西升级到2.7。我最终会谈到它,但我可能会尽快提供一些线索继续下去。 - Adam Crossland


答案:


当我从0.96转换为1.2 Django模板时,这个问题也让我感到困惑。当SDK 1.4.2开始发出我需要选择版本的警告时,我最初被推动这样做,但当我研究模板语言中急需的改进时,我急于做出改变。

然后一切都破了。和你一样,我在我身上使用了很多相对路径 extends 和 include 命令。它花了很多调试和挖掘,但我确实找出了问题的原因和一个非常好的解决方案。

原因:在Django 1.2中,加载模板文件的代码使用一个名为的命令启动 safe_join 加入路径部分(你可以看到代码) google_appengine\lib\django_1_2\django\template\loaders\filesystem.py)。它不允许相对路径超出它认为的顶级目录。这与配置为阻止您访问服务器的整个文件系统的Web服务器相同 ..进入你的网址。最终的结果是

{% extends "../templates/base.html" %}

曾经很好的打破规则,它不会起作用。

我在我的应用程序中修复此问题而不完全重构我的模板布局的方法是实现自定义 TemplateLoader。 Django的模板渲染引擎允许应用程序拥有许多不同的类,这些类知道如何以不同的方式查找模板。如果你查看我上面给出的目录,你会看到提供了几个,它们都是继承自的类 BaseLoader。我提供了自己的,我的模板是如何定制的。

我的项目有类似Rails的布局:

app/
   controllers/
      home_controller.py
      posts_controller.py
   models/
      ...
   views/
      home/
          index.html
          about.html
      posts/
          show.html
          new.html
      shared/
          base.html
          post.html

每个模板都扩展 base.html 还有一对包括 post.html,他们以前使用相对路径到达他们的位置 base/。理想情况下,我甚至不想使用 .. up-dir到达那里,但需要0.96。我创建了以下模板加载器来使用我的方案:

from django.conf import settings
from django.template import TemplateDoesNotExist
from django.template.loader import BaseLoader
from django.utils._os import safe_join
import os

class MvcTemplateLoader(BaseLoader):
    "A custom template loader for the MVCEngine framework."

    is_usable = True

    __view_paths = None

    def __init__(self, views_path):
        self.views_path = views_path
        # We only need to instantiate the view_paths class variable once.
        if MvcTemplateLoader.__view_paths is None:
            temp_paths = []
            for each_path in os.listdir(views_path):
                # We want to skip hidden directories, so avoid anything that starts with .
                # This works on both Windows and *NIX, but could it fail for other OS's?
                if not each_path.startswith('.'):
                    full_path = os.path.join(views_path, each_path)
                    if each_path == "shared":
                        # The shared directory is special. Since templates in many other directories will be
                        # inheriting from or including templates there, it should come second, right after the
                        # root views directory. For now, it will be first.
                        temp_paths.insert(0, full_path)
                    else:
                        temp_paths.append(full_path)
            # The root views_path itself will always be first in order to give resolution precendence to templates
            # that are specified with a parent directory. In other words, home/index.html will be immediately
            # resolved with no ambiguity; whereas, index.html could resolve as bar/index.html rather than
            # foo/index.html.
            temp_paths.insert(0, views_path)
            MvcTemplateLoader.__view_paths = temp_paths


    def get_template_sources(self, template_name):
        for template_dir in MvcTemplateLoader.__view_paths:
            try:
                yield safe_join(template_dir, template_name)
            except UnicodeDecodeError:
                # The template dir name was a bytestring that wasn't valid UTF-8.
                raise
            except ValueError:
                # The joined path was located outside of this particular
                # template_dir (it might be inside another one, so this isn't
                # fatal).
                pass

    def load_template_source(self, template_name, template_dirs=None):
        tried = []
        for filepath in self.get_template_sources(template_name):
            try:
                file = open(filepath)
                try:
                    return (file.read().decode(settings.FILE_CHARSET), filepath)
                finally:
                    file.close()
            except IOError:
                tried.append(filepath)

        error_msg = "Could not find %s in any of the views subdirectories." % template_name
        raise TemplateDoesNotExist(error_msg)
    load_template_source.is_usable = True

_loader = MvcTemplateLoader

我使我的自定义模板加载器包含在Django通过更改我的应用程序尝试的集合中 main 功能看起来像这样:

def main():    
    from google.appengine.dist import use_library
    use_library('django', '1.2')

    os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'

    from django.conf import settings 
    views_path = os.path.join(os.path.dirname(__file__), 'app','views')
    settings.TEMPLATE_LOADERS = (('gaemvclib.mvctemplateloader.MvcTemplateLoader', views_path), 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader')

(然后通常进入你的主要功能的所有其他东西)。

所以,我认为您应该能够修改上面的TemplateLoader代码以匹配模板目录的布局,并且它不仅可以让您更好地控制模板层次结构的布局,还可以更好地控制 extends 和 include 声明。你不再使用了 .. 而只是给出模板的路径相对于项目中的任何东西相当于我的 views 目录。


15
2018-03-10 20:57



谢谢。非常感谢。 - probably at the beach
运用 self.response.out.write(template.render(path, template_values)) 使用MvcTemplateLoader类给出了这个错误“ValueError:需要多于1个值才能解压”为什么会这样? - zakdances
Zak,所有这些技术可能已经被1.5.5版本中最近的更改所取代。我没有机会针对这些变化测试我自己的应用程序 - 我已经破坏了AppEngine Go runtine - 但发行说明告诉我渲染区域已经发生了很大的变化。我会调查一下,尽快回复你。 - Adam Crossland
@Adam Crossland我已经使用了你的方法,现在尝试将我的代码移植到python2.7,除了用于渲染模板的正确路径之外,一切都已设置。组织各种深度的文件至关重要,至少对我来说要保持井井有条。我期待你的更新:) - topless
@ Chris-Top,你能告诉我你在获得正确途径时遇到了什么问题吗?我最近一直在使用AppEngine的Go运行时的项目,所以我甚至没想过将我的Python东西升级到2.7。我最终会谈到它,但我可能会尽快提供一些线索继续下去。 - Adam Crossland


您收到错误消息的一个原因 TemplateDoesNotExist: 模板\ AdminListstr.html

双击“模板”和模板名称之间使用的斜杠。我浪费了几个小时,然后检查了os.path.join返回的值。

总之,您需要确保使用“/”而不是Windows“\”。如果使用错误的斜杠,则代码将在(Windows)开发中起作用,但在部署时会失败。

我的诊断代码打印了这个:查看最后一个斜杠 os.path.join返回的值是:/ base/data/home/apps/s~myapp/1.356037954289984934/templates \ LabListstr.html

Aye yi yi!


-2
2018-01-12 05:32