扩展模版系统

作者: 大爷的二舅 | 来源:发表于2018-02-06 14:01 被阅读2次

    现在您已经对模板系统的内部有了更多的了解,我们来看看如何使用自定义代码来扩展系统。 大多数模板自定义都以自定义模板标签和/或过滤器的形式出现。 尽管Django模板语言带有许多内置的标签和过滤器,但是您可能会组装自己的符合自己需求的标签和过滤器库。 幸运的是,定义自己的功能非常容易。

    代码布局

    自定义模板标签和过滤器必须存在于Django应用程序中。 如果他们涉及到现有的应用程序是有意义的捆绑在那里; 否则,你应该创建一个新的应用程序来保存它们。 应用程序应该包含一个templatetags目录,与models.py,views.py等级相同。如果这个目录不存在,创建它 - 不要忘记init.py文件以确保目录被视为 一个Python包。

    添加此模块后,您需要重新启动服务器,然后才能在模板中使用标记或过滤器。 您的自定义标签和过滤器将位于templatetags目录中的模块中。

    模块文件的名称是稍后用于加载标签的名称,因此请谨慎选择不会与其他应用中的自定义标签和过滤器冲突的名称。

    例如,如果您的自定义标签/过滤器位于名为review_extras.py的文件中,则您的应用程序布局可能如下所示:

    reviews/
        __init__.py
        models.py
        templatetags/
            __init__.py
            review_extras.py
        views.py 
    

    在你的模板中,你可以使用下面的代码:

    {% load review_extras %}
    

    包含自定义标记的应用程序必须位于INSTALLED_APPS中才能使{%load%}标记正常工作。

    在大量的例子中,阅读Django默认过滤器和标签的源代码。 它们分别在django / template / defaultfilters.py和django / template / defaulttags.py中。有关加载标签的更多信息,请阅读它的文档。

    创建一个模板库

    无论您是在编写自定义标签还是过滤器,首先要做的就是创建一个模板库 - Django可以连接的一小部分基础架构。

    创建一个模板库是一个两步过程:

    • 首先,决定哪个Django应用程序应该放置模板库。 如果您已经通过manage.py startapp创建了一个应用程序,则可以将其放在那里,也可以仅为模板库创建另一个应用程序。 我们建议后者,因为你的过滤器可能在未来的项目中对你有用。 无论您选择哪条路线,都要确保将该应用添加到您的INSTALLED_APPS设置中。 我马上解释一下。
    • 其次,在合适的Django应用程序包中创建一个templatetags目录。 它应该与models.py,views.py等等级相同。 例如:
    books/
         __init__.py
         models.py
         templatetags/
         views.py
    

    在templatetags目录中创建两个空文件:一个init.py文件(向Python表明这是一个包含Python代码的包)和一个包含自定义标记/过滤器定义的文件。 后一个文件的名称是您稍后用来加载标签的名称。 例如,如果您的自定义标签/过滤器位于名为review_extras.py的文件中,则可以在模板中输入以下内容:

    {% load review_extras %}
    

    {%load%}标记查看您的INSTALLED_APPS设置,仅允许在已安装的Django应用程序中加载模板库。这是一个安全功能;它允许您在单台计算机上托管许多模板库的Python代码,而不必为每个Django安装都访问它们。

    如果您编写的模板库不受限于任何特定的模型/视图,那么使用仅包含templatetags包的Django应用程序包是有效且正常的。

    模板标签包中放置了多少个模块没有限制。请记住,{%load%}语句将为给定的Python模块名称加载标签/过滤器,而不是应用程序的名称。

    一旦你创建了Python模块,你将只需要编写一些Python代码,这取决于你是在编写过滤器还是标签。要成为有效的标签库,模块必须包含一个名为register的模块级变量,该变量是template.Library的一个实例。

    这是注册所有标签和过滤器的数据结构。所以,靠近你的模块的顶部,插入以下内容:

    from django import template
    
    register = template.Library()
    
    自定义模板标签和过滤器

    Django的模板语言提供了各种各样的内置标签和过滤器,旨在满足应用程序的表示逻辑需求。 不过,您可能会发现自己需要的功能不在核心模板原语集范围内。

    您可以通过使用Python定义自定义标记和过滤器来扩展模板引擎,然后使用{%load%}标记使其可用于您的模板。

    编写自定义模板过滤器

    自定义过滤器只是带有一个或两个参数的Python函数:

    1. 变量的值(输入) - 不一定是一个字符串。
    2. 参数的值 - 这可以有一个默认值,或完全省略。

    例如,在过滤器{{var | foo:“bar”}}中,过滤器foo将被传递变量var和参数“bar”。 由于模板语言不提供异常处理,因此模板过滤器引发的任何异常都将作为服务器错误公开。

    因此,如果有合理的回退值返回,过滤器函数应该避免引发异常。 如果输入表示模板中有明确的错误,则引发异常仍然可能比隐藏错误的静默失败更好。 以下是一个示例过滤器定义:

    def cut(value, arg):
        """Removes all values of arg from the given string"""
        return value.replace(arg, '')
    

    下面是一个如何使用这个过滤器的例子:

    {{somevariable | cut:“0”}}
    

    大多数过滤器不需要参数。 在这种情况下,请将参数从函数中删除。 例:

    def lower(value): # Only one argument.
        """Converts a string into all lowercase"""
        return value.lower()
    
    注册自定义过滤器

    一旦你编写了你的过滤器定义,你需要注册它的库实例,使其可用于Django的模板语言:

    register.filter('cut', cut)
    register.filter('lower', lower)
    

    Library.filter()方法有两个参数:

    1. 过滤器的名称 - 一个字符串。
    2. 编译函数 - 一个Python函数(不是作为字符串的函数的名字)。

    您可以使用register.filter()作为装饰器:

    @register.filter(name='cut')
    def cut(value, arg):
        return value.replace(arg, '')
    
    @register.filter
    def lower(value):
        return value.lower()
    

    如果你忽略名称参数,就像在上面的第二个例子中一样,Django将使用该函数的名字作为过滤器名称。 最后,register.filter()也接受三个关键字参数:is_safe,needs_autoescape和expects_localtime。 这些参数在下面的过滤器和自动转义和过滤器以及时区中进行了描述。

    预期字符串的模板过滤器

    如果你正在编写一个模板过滤器,只需要一个字符串作为第一个参数,你应该使用装饰器的字符串过滤器。 这将在传递给你的函数之前将对象转换为字符串值:

    from django import template
    from django.template.defaultfilters import stringfilter
    
    register = template.Library()
    
    @register.filter
    @stringfilter
    def lower(value):
        return value.lower()
    

    这样,你就可以将一个整数传递给这个过滤器,并且不会导致AttributeError(因为整数没有lower()方法)。

    过滤器和自动转义

    在编写自定义过滤器时,请考虑过滤器如何与Django的自动转义行为进行交互。 请注意,可以在模板代码中传递三种类型的字符串:

    • 原始字符串是本地Python str或unicode类型。 在输出时,如果自动转义生效并且保持不变,则会被转义。
    • 安全字符串是已标记为安全的字符串,以免在输出时进一步转义。 任何必要的转义已经完成。 它们通常用于包含原始HTML的输出,这些原始HTML将被解释为在客户端。

    在内部,这些字符串是SafeBytes或SafeText类型。 他们共享一个共同的SafeData基类,所以你可以使用如下代码对它们进行测试:

     if isinstance(value, SafeData):
           
        # Do something with the "safe" string.
           
        ...
    
    • 标记为“需要转义”的字符串总是在输出上转义,而不管它们是否在autoescape块中。 这些字符串只能被转义一次,但是,即使应用了自动转义。 在内部,这些字符串的类型是EscapeBytes或EscapeText。 一般来说,你不必担心这些。 它们存在用于执行转义过滤器。

    模板过滤器代码有两种情况:

    1. 您的过滤器不会将任何不安全的HTML字符(<,>,',“或&)引入到尚不存在的结果中;或者
    2. 或者,您的过滤器代码可以手动处理任何必要的转义。 当您将新的HTML标记引入结果时,这是必需的。

    在第一种情况下,您可以让Django为您处理所有自动转义处理。 所有你需要做的就是当你注册你的过滤器函数时,将is_safe标志设置为True,如下所示:

    @register.filter(is_safe=True)
    def myfilter(value):
        return value 
    

    这个标志告诉Django,如果一个“安全的”字符串被传入你的过滤器,结果仍然是“安全的”,如果一个非安全的字符串被传入,Django将自动转义它,如果有必要的话。 你可以认为这是“这个过滤器是安全的 - 它不会引入任何不安全的HTML的可能性”。

    原因is_safe是必要的,因为有很多正常的字符串操作会把一个SafeData对象变成一个普通的str或unicode对象,而不是试图去捕捉所有的,这将是非常困难的,Django修复后的损害 过滤器已经完成。

    例如,假设您有一个过滤器,将字符串xx添加到任何输入的末尾。
    由于这不会对结果引入危险的HTML字符(除了已经存在的字符),您应该用is_safe标记您的过滤器:

    @register.filter(is_safe=True)
    def add_xx(value):
        return '%sxx' % value 
    

    当这个过滤器被用在启用自动转义的模板中时,只要输入没有被标记为“安全”,Django就会转义输出。默认情况下,is_safe是False,您可以从任何不需要的过滤器中忽略它。在决定你的过滤器是否真的离开安全的字符串时要小心。如果您要删除字符,可能会在结果中不经意间留下不平衡的HTML标记或实体。

    例如,从输入中删除>可能会将<a>变成<a,这将需要在输出上进行转义以避免导致问题。同样,删除分号(;)可以转向&amp;进&amp;,这不再是一个有效的实体,因此需要进一步逃避。大多数情况下几乎不会这么棘手,但在查看代码时请留意这类问题。

    标记一个过滤器is_safe会强制过滤器的返回值为一个字符串。如果你的过滤器应该返回一个布尔值或其他非字符串值,那么标记is_safe可能会产生意想不到的后果(比如将一个布尔型的False转换为“False”字符串)。

    在第二种情况下,您希望将输出标记为安全,避免进一步转义,以便您的HTML标记不会进一步转义,因此您需要自己处理输入。要将输出标记为安全字符串,请使用
    django.utils.safestring.mark_safe()。

    不过要小心。您需要做的不仅仅是将输出标记为安全。你需要确保它确实是安全的,你做什么取决于自动转义是否有效。

    这个想法是编写可以在自动转义开启或关闭的模板中运行的过滤器,以便让模板作者更容易。

    为了让您的过滤器知道当前的自动转义状态,请在注册过滤器函数时将needs_autoescape标志设置为True。 (如果您不指定此标志,则默认为False)。这个标志告诉Django你的过滤器函数想要传递一个额外的关键字参数,称为autoescape,如果自动转义生效则为True,否则为False。

    例如,让我们编写一个强调字符串的第一个字符的过滤器:

    from django import template
    from django.utils.html import conditional_escape
    from django.utils.safestring import mark_safe
    
    register = template.Library()
    
    @register.filter(needs_autoescape=True)
    def initial_letter_filter(text, autoescape=None):
        first, other = text[0], text[1:]
        if autoescape:
            esc = conditional_escape
        else:
            esc = lambda x: x
        result = '<strong>%s</strong>%s' % (esc(first), esc(other))
        return mark_safe(result)
    

    needs_autoescape标志和autoescape关键字参数意味着我们的函数将知道当过滤器被调用时自动转义是否有效。我们使用autoescape来决定输入数据是否需要通过django.utils.html.conditional_escape传递。 (在后一种情况下,我们只是使用身份函数作为“转义”函数。)

    conditional_escape()函数就像escape(),除了它只是转义不是SafeData实例的输入。如果将SafeData实例传递给conditional_escape(),则数据将保持不变。

    最后,在上面的例子中,我们记得将结果标记为安全的,以便我们的HTML直接插入到模板中,而不会进一步转义。在这种情况下,不需要担心is_safe标志(尽管包括它不会伤害任何东西)。无论何时您手动处理自动转义问题并返回一个安全字符串,is_safe标志都不会改变任何方式。

    过滤器和时区

    如果你编写一个自定义的过滤器,在日期时间对象上运行,你通常会注册它的expects_localtime标志设置为True:

    @register.filter(expects_localtime=True)
    def businesshours(value):
        try:
            return 9 <= value.hour < 17
        except AttributeError:
            return ''
    

    设置此标志时,如果过滤器的第一个参数是可识别时区的日期时间,则根据模板中时区转换的规则,Django会在适当时将其转换为当前时区,然后将其转换为过滤器。

    重复使用内置过滤器时避免XSS漏洞在重复使用Django的内置过滤器时要小心。 您需要将autoescape = True传递给筛选器,以便获得正确的自动转义行为并避免跨站点脚本漏洞。 例如,如果您想编写一个名为urlize_and_linebreaks的自定义过滤器,它将urlize和linebreaksbr过滤器组合在一起,过滤器将如下所示:

    from django.template.defaultfilters import linebreaksbr,urlize
    @ register.filter
    def urlize_and_linebreaks(text):
    `return linebreaksbr(
    urlize(text,autoescape = True),autoescape = True
    )`
    然后:{{comment | urlize_and_linebreaks}}
    将相当于:
    {{comment | urlize | linebreaksbr}}
    
    编写自定义模板标签

    标签比过滤器更复杂,因为标签可以做任何事情。 Django提供了一些快捷方式,使得大多数类型的标签更容易编写。首先,我们将探索这些快捷方式,然后解释如何在快捷方式不够强大的情况下从头开始编写标签。

    简单的标签

    许多模板标签需要一些参数(字符串或模板变量),并在完成一些基于输入参数和外部信息的处理后返回结果。

    例如,current_time标记可能接受格式字符串,并将时间作为相应的格式化字符串返回。为了简化这些类型的标签的创建,Django提供了一个辅助函数simple_tag。这个函数是django.template.Library的一个方法,它接受一个接受任意数量参数的函数,把它包装在一个渲染函数和上面提到的其他必要位中,并将其注册到模板系统中。

    我们的current_time函数可以这样写:

    import datetime
    from django import template
    
    register = template.Library()
    
    @register.simple_tag
    def current_time(format_string):
        return datetime.datetime.now().strftime(format_string)
    

    关于simple_tag帮助函数的一些注意事项:

    • 检查所需数量的参数等已经在调用函数的时候完成,所以我们不需要这样做。

    • 参数周围的引号(如果有的话)已经被删除了,所以我们只收到一个纯字符串。

    • 如果参数是一个模板变量,我们的函数将传递变量的当前值,而不是变量本身。

    如果您的模板标签需要访问当前的上下文,那么在注册您的标签时可以使用takes_context参数:

    @register.simple_tag(takes_context=True)
    def current_time(context, format_string):
        timezone = context['timezone']
        return your_get_current_time_method(timezone, format_string)
    

    请注意,第一个参数必须被称为上下文。 有关take_context选项如何工作的更多信息,请参阅包含标签部分。 如果您需要重命名标签,则可以为其提供一个自定义名称:

    register.simple_tag(lambda x: x - 1, name='minusone')
    
    @register.simple_tag(name='minustwo')
    def some_function(value):
        return value - 2 
    

    simple_tag函数可以接受任意数量的位置或关键字参数。 例如:

    @register.simple_tag
    def my_tag(a, b, *args, **kwargs):
        warning = kwargs['warning']
        profile = kwargs['profile']
        ...
        return ...
    

    然后在模板中,任何数量的由空格分隔的参数都可以被传递给模板标签。 和Python一样,关键字参数的值是用等号(“=”)设置的,必须在位置参数之后提供。 例如:

    {% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}
    
    包含标签

    另一种常见的模板标签类型是通过渲染另一个模板来显示一些数据的类型。 例如,Django的管理界面使用自定义模板标签来显示“添加/更改”表单页面底部的按钮。 这些按钮总是看起来一样,但是链接目标根据被编辑的对象而改变 - 所以它们是使用充满来自当前对象的细节的小模板的理想情况。 (在管理员的情况下,这是submit_row标签。)

    这些标签被称为包含标签。 编写包含标签可能是最好的例子。 让我们写一个标签,为给定的作者对象生成书籍列表。 我们将使用这样的标签:

    {% books_for_author author %}
    

    结果会是这样的:

    <ul>
        <li>The Cat In The Hat</li>
        <li>Hop On Pop</li>
        <li>Green Eggs And Ham</li>
    </ul>
    

    首先,我们定义接受参数的函数,并为结果生成一个数据字典。 请注意,我们只需要返回一个字典,而不是更复杂。 这将被用作模板片段的上下文:

    def books_for_author(author):
        books = Book.objects.filter(authors__id=author.id)
        return {'books': books}
    

    接下来,我们创建用于呈现标记输出的模板。 按照我们的例子,模板非常简单:

    <ul>
    {% for book in books %}<li>{{ book.title }}</li>
    {% endfor %}
    </ul>
    

    最后,我们通过调用Library对象上的inclusion_tag()方法来创建并注册包含标签。 在我们的例子之后,如果前面的模板在一个名为book_snippet.html的文件中被模板加载器搜索到的目录中,我们注册这个标签:

    # Here, register is a django.template.Library instance, as before
    @register.inclusion_tag('book_snippet.html')
    def show_reviews(review):
        ...
    

    或者,可以在首次创建函数时使用django.template.Template实例注册包含标记:

    from django.template.loader import get_template
    t = get_template('book_snippet.html')
    register.inclusion_tag(t)(show_reviews)
    

    有时,你的包含标签可能需要大量的参数,这使得模板作者传递所有参数并记住它们的顺序是一件很痛苦的事情。 为了解决这个问题,Django为包含标签提供了一个takes_context选项。 如果在创建一个包含标签时指定了takes_context,那么该标签将没有必要的参数,而底层的Python函数将会有一个参数:模板上下文以及标签被调用的时间。 例如,假设您正在编写一个包含标签,该标签将始终用于包含指向主页面的home_link和home_title变量的上下文中。 以下是Python函数的样子:

    @register.inclusion_tag('link.html', takes_context=True)
    def jump_link(context):
        return {
            'link': context['home_link'],
            'title': context['home_title'],
        }
    

    (请注意,函数的第一个参数必须称为上下文。)
    模板link.html可能包含以下内容:

    Jump directly to <a href="{{ link }}">{{ title }}</a>.
    

    然后,任何时候你想使用这个自定义标签,加载它的库,并调用它没有任何参数,如下所示:

    {% jump_link %}
    

    请注意,当您使用takes_context = True时,不需要将参数传递给模板标签。 它会自动获取上下文。 takes_context参数默认为False。
    当它被设置为True时,标签被传递给上下文对象,如本例所示。 这是这种情况和以前的inclusion_tag示例之间的唯一区别。 像simple_tag一样,inclusion_tag函数也可以接受任意数量的位置或关键字参数。

    作业标签

    为了简化在上下文中设置变量的标签的创建,Django提供了一个辅助函数assign_tag。 这个函数的工作方式与simple_tag()方法相同,只是它将标记的结果存储在指定的上下文变量中,而不是直接输出。 我们之前的current_time函数可以写成这样:

    @register.assignment_tag
    def get_current_time(format_string):
        return datetime.datetime.now().strftime(format_string)
    

    然后,您可以使用as参数和变量名称将结果存储在模板变量中,并在您认为合适的地方自行输出:

    {% get_current_time "%Y-%m-%d %I:%M %p" as the_time %}
    <p>The time is {{ the_time }}.</p>
    

    相关文章

      网友评论

        本文标题:扩展模版系统

        本文链接:https://www.haomeiwen.com/subject/wgjgzxtx.html