Skip to content

Latest commit

 

History

History
945 lines (703 loc) · 37.8 KB

05.md

File metadata and controls

945 lines (703 loc) · 37.8 KB

第5章 自定义模板过滤器和标签

本章中包含如下小节:

  • 遵循惯例自建模板过滤器和标签
  • 创建模板过滤器来在文章发布后显示几天前
  • 创建模板过滤器提取第一个媒体对象
  • 创建模板过滤器来让 URL 易读
  • 创建模板标签在模板存在时包含它
  • 创建模板标签来加载模板中的查询集
  • 创建模板标签来解析内容为模板
  • 创建模板标签来修改请求查询参数

引言

Django有一套完善的模板系统,具有模板继承、修改值展现的过滤器、用于展示逻辑的标签等功能。此外,Django允许我们对应用添加自定义的模板过滤器和标签。自定义过滤器或标签应放在应用的templatetags Python包下的template-tag库文件中。然后template-tag库可通过{% load %}模板标签在所有模板中进行载入。本章中,我们将创建一些过滤器和模板来对模板编辑器进行更进一步的控制。

技术要求

运行本章的代码要求安装最新稳定版的Python 3、MySQL或PostgreSQL数据库以及通过虚拟环境创建的Django项目。

可在GitHub仓库的Chapter05目录中查看本章的代码。

遵循惯例自建模板过滤器和标签

如果没有章法,自定义模板过滤器和标签会容易混淆、不一致。对模板编辑器提供尽可能多的灵活方便的模板过滤器很有必要。本节中我们将学习一些增强Django模板系统功能遵循的规则:

  1. 在页面逻辑更适合放在视图、上下文处理器或模型方法中时不要创建或使用自定义模板过滤器、标签。在内容涉及具体上下文时,如对象列表或对象详情视图,在视图中加载对象。如果需要在大部分页面显示某个内容,创建一个上下文处理器。在需要获取与模板上下文无关的某些对象属性时使用自定义模型方法来替代模板过滤器。

  2. 以_tags为后缀对template-tag库进行命名。模板标签库与应用命名不同时可以避免模棱两可的包导入问题。

  3. 在新建的库中,将过滤器与标签分隔开,例如,像下面代码这样用注释分开:

    # myproject/apps/core/templatetags/utility_tags.py
    

​ from django import template


register = template.Library()

 """ TAGS """

# Your tags go here...

""" FILTERS """

# Your filters go here...
```
  1. 在创建高级自定义模板标签时,通过在标签名后包含如下结构来让语法更容易记忆:

    • for [app_name.model_name]:包含这一结构来使用指定的模型
    • using [template_name]:包含这一结构来对模板标签的输出使用一个模板
    • limit [count]:包含这一结构来限制结果为指定数量
    • as [context_variable]:包含这一结构来在可复用多次的上下文变量中存储结果
  2. 如非一目了然避免在模板标签定义多个位置参数值。否则,很容易让模板开发者产生混淆。

  3. 生成尽可能多的可解析参数。不带引号的字符中应视作需要解析的上下文变量,或是template-tag产品信息结构的短词。

创建模板过滤器来在文章发布后显示几天前

在涉及创建或修改日期时,显示为人类更易读的时间差会更为方便,比如,博客文章发布于3天前、新闻文章发布于今天、用户于昨天登录。本节中,我们将创建一个模板过滤器date_since,它将日期转化为人类更易读的按天、星期、月或年的时间差。

准备工作

如未创建core应用,先创建并将其添加至设置文件的INSTALLED_APPS中。然后在该应用中创建一个templatetags Python包(Python包是一个包含空__init__.py文件的目录)。

如何实现...

创建utility_tags.py文件并添加如下内容:

# myproject/apps/core/templatetags/utility_tags.py
from datetime import datetime
from django import template
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _

register = template.Library()

""" FILTERS """
DAYS_PER_YEAR = 365 
DAYS_PER_MONTH = 30 
DAYS_PER_WEEK = 7

@register.filter(is_safe=True) 
def date_since(specific_date):
  """
  Returns a human-friendly difference between today and past_date (adapted from https://www.djangosnippets.org/snippets/116/)
  """
  today = timezone.now().date()
  if isinstance(specific_date, datetime):
    specific_date = specific_date.date() 
  diff = today - specific_date
  diff_years = int(diff.days / DAYS_PER_YEAR) 
  diff_months = int(diff.days / DAYS_PER_MONTH) 
  diff_weeks = int(diff.days / DAYS_PER_WEEK) 
  diff_map = [
    ("year", "years", diff_years,), 
    ("month", "months", diff_months,), 
    ("week", "weeks", diff_weeks,), 
    ("day", "days", diff.days,),
  ]
  for parts in diff_map:
    (interval, intervals, count,) = parts 
    if count > 1:
      return _(f"{count} {intervals} ago") 
    elif count == 1:
      return _("yesterday") \
        if interval == "day" \ 
        else _(f"last {interval}")
  if diff.days == 0: 
    return _("today")
  else:
    # Date is in the future; 
    return formatted date. return f"{specific_date:%B %d, %Y}"

实现原理...

模板所使用的这个过滤器会渲染出类似昨天、上周或5个月前这样的信息,如以下代码所下:

{% load utility_tags %}
{{ object.published|date_since }}

可以对date及datetime类型的值应用这一过滤器。

每个模板标签库都有一个对汇集了过滤器和标签的template.Library类型的注册。Django过滤器是由@register.filter装饰器注册的函数。在本例中,我们传递了is_safe=True参数来表明过滤器不会引用不安全的HTML标记。

默认,模板系统中的过滤器会和函数或其它可调用对象同名。可通过对装饰器传递名称来使用其它名称,如下:

@register.filter(name="humanized_date_since", is_safe=True) 
def date_since(value):
  #...

过滤器自身已经很直白了。首先读取当前日期。如果给定值为datetime类型,过滤器会提取出日期。然后会根据DAYS_PER_YEAR、DAYS_PER_MONTH、DAYS_PER_WEEK或天数间隔计算出提取值与当天的差值。根据计数不同,会返回不同的字符串结果,如果值还要更久则显示为格式化后的日期。

扩展知识...

如果需要我们也可以函数其它范围的时间,如20分钟前、5小时前或十天(decade)前。这时只需要对已有的diff_map集合添加更多的间隔,显示时间差需要把日期值换作日期时间值进行操作。

相关内容

  • 创建模板过滤器提取第一个媒体对象一节
  • 创建模板过滤器来让 URL 易读一节

创建模板过滤器提取第一个媒体对象

设想一下你在开发一个博客综合页,希望在该页显示每篇文章内容中的图片、音频或视频。这时,需要从文章的HTML内容中提取出<figure><img><object><embed><video><audio><iframe>标签,内容存储在文章模型的一个字段中。本节中,我们将学习如何在first_media过滤器中使用正则表达式来执行这一操作。

准备工作

我们使用core应用,应在配置文件的INSTALLED_APPS中进行过添加,并且应用中应包含templatetags包。

如何实现...

在utility_tags.p文件中,添加如下内容:

# myproject/apps/core/templatetags/utility_tags.py
import re
from django import template
from django.utils.safestring import mark_safe
register = template.Library()

""" FILTERS """

MEDIA_CLOSED_TAGS = "|".join(["figure", "object", "video", "audio", "iframe"])
MEDIA_SINGLE_TAGS = "|".join(["img", "embed"]) MEDIA_TAGS_REGEX = re.compile(
  r"<(?P<tag>" + MEDIA_CLOSED_TAGS + ")[\S\s]+?</(?P=tag)>|" + 
  r"<(" + MEDIA_SINGLE_TAGS + ")[^>]+>",
  re.MULTILINE)

@register.filter
def first_media(content):
  """
  Returns the chunk of media-related markup from the html content 
  """
  tag_match = MEDIA_TAGS_REGEX.search(content)
  media_tag = ""
  if tag_match:
    media_tag = tag_match.group() 
  return mark_safe(media_tag)

实现原理...

如果数据库中存在HTML内容,将如下代码放到模板中会从对象的内容字段中提取媒体标签;而在未找到媒体内容时会返回空字符串:

{% load utility_tags %}
{{ object.content|first_media }}

正则表达式是用于搜索或替换文本模式的强大功能。首先,我们定义所支持的所有媒体标签名列表,将其分组为具有开闭标签的(MEDIA_CLOSED_TAGS)及自闭合标签(MEDIA_SINGLE_TAGS)的组。通过这些列表,生成编译后的正则表达式为MEDIA_TAGS_REGEX。在本例中,我们搜索所有可用的媒体标签,允许它们出现在多行中。

我们来了解下正则表达式的原理,如下:

  • 可选模式由管道符(|) 进行分隔。
  • 模式中有两个分组,其一是包含常规开闭标签(<figure><object><video><audio><iframe><picture>)的分组,另一个是称为自闭合或void标签(和)的最终模式分组。
  • 对于可能为多行的常规标签,我们使用 [\S\s]+?来至少匹配一次任意符号,但我们在发现其后存在的字符串前让匹配次数尽量少。
  • 因此<figure[\S\s]+?</figure>搜索<figure>标签的开头以及其后的内容,直至找到闭合标签为止。
  • 类似的地[^>]+模式用于自闭合标签,我们搜索除右尖括号(可能更多人熟知其为大于号,即>)之后的任意符号,至少一次,尽可能多的次数,直至碰到表明为关闭标签的尖括号为止。

re.MULTILINE标记保证即使内容位于多选也可以查找到匹配内容。然后在该过滤器中,我们使用这一正则表达式模式执行搜索。默认在Django中,任意过滤器的结果中会将<、>及&符号分别转义为<、>及&实例。但在本例中,我们使用了mark_safe()函数来表明结果是安全并且HTML就绪的,因此所有内容都不进行转义而渲染。因为原始内容由用户输入,我们在注册过滤器时转而传递is_safe=True,因为我们需要显式地验证标记是安全的。

扩展知识...

如查想要了解正则表达式相关知识,可以阅读Python官方文档来进行学习。

相关内容

  • 创建模板过滤器来在文章发布后显示几天前一节
  • 创建模板过滤器来让 URL 易读一节

创建模板过滤器来让 URL 易读

Web用户通常认识不带协议(http://) 或结尾斜杠 (/)的URL,类似地他们会按照这种方式在地址栏中输入URL。本节中,我们将创建一个humanize_url过滤器来对长地址进行截取以更短的格式向用户呈现URL,类似Twitter推文中的链接。

准备工作

类似前面小节,我们使用core应用,应将其添加至配置文件的INSTALLED_APPS中,并且其中应包含templatetags包。

如何实现...

在core应用utility_tags.py 模板库的的FILTERS版块中,添加humanize_url过滤器并进行注册,如以下代码所示:

# myproject/apps/core/templatetags/utility_tags.py
import re
from django import template

register = template.Library()

""" FILTERS """

@register.filter
def humanize_url(url, letter_count=40):
  """
  Returns a shortened human-readable URL
  """
  letter_count = int(letter_count)
  re_start = re.compile(r"^https?://") 
  re_end = re.compile(r"/$")
  url = re_end.sub("", re_start.sub("", url)) 
  if len(url) > letter_count:
    url = f"{url[:letter_count - 1]}..." 
  return url

实现原理...

我们可以在所有模板中使用humanize_url过滤器,如下:

{% load utility_tags %}
<a href="{{ object.website }}" target="_blank">
{{ object.website|humanize_url }} </a>
<a href="{{ object.website }}" target="_blank"> {{ object.website|humanize_url:30 }}
</a>

该过滤器使用正则表达式来删除前置协议后尾部斜杠,将URL缩短为给定字母数(默认为40),并且在完整URL超出指定字符数时进行截取、在尾部添加省略号。例如,对于链接https://docs.djangoproject.com/en/3.0/howto/custom-template-tags/ ,40位字符的易读版本为docs.djangoproject.com/en/3.0/howto/cus...。

相关内容

  • 创建模板过滤器来在文章发布后显示几天前一节
  • 创建模板过滤器提取第一个媒体对象一节
  • 创建模板标签在模板存在时包含它一节

创建模板标签在模板存在时包含它

Django提供{% include %}模板标签,允许在一个模板中渲染并包含其它模板。但是在尝试包含文件系统中不存在的模板时模板标签会抛出错误。本节中,我们将创建一个包含另一个模板的{% try_to_include %}模板标签,如模板不存在则默默地将其渲染为空字符串。

准备工作

我们会再次使用安装了的core应用,在其中添加自定义模板标签。

如何实现...

执行如下步骤来创建{% try_to_include %}模板标签:

  1. 首先创建解析模板标签参数的函数,如下:

    # myproject/apps/core/templatetags/utility_tags.py
    from django import template
    from django.template.loader import get_template
    
    register = template.Library() 
    
    """ TAGS """
    
    @register.tag
    def try_to_include(parser, token):
      """
      Usage: {% try_to_include "some_template.html" %}
    
      This will fail silently if the template doesn't exist.
      If it does exist, it will be rendered with the current context.
      """
      try:
        tag_name, template_name = token.split_contents() 
      except ValueError:
        tag_name = token.contents.split()[0] 
        raise template.TemplateSyntaxError(
          f"{tag_name} tag requires a single argument") 
      return IncludeNode(template_name)
    
  2. 然后在同一个文件中需要一个继承自基础template.Node的自定义IncludeNode类。我们将其插入到try_to_include()函数 的前面,如下:

    class IncludeNode(template.Node):
      def __init__(self, template_name):
        self.template_name = template.Variable(template_name)
    
      def render(self, context):
        try:
          # Loading the template and rendering it
          included_template = self.template_name.resolve(context) 
          if isinstance(included_template, str):
            included_template = get_template(included_template) 
          rendered_template = included_template.render(
            context.flatten() 
          )
        except (template.TemplateDoesNotExist, 
          template.VariableDoesNotExist,
          AttributeError): 
          rendered_template = ""
        return rendered_template
    
    @register.tag
    def try_to_include(parser, token):
      #...
    

实现原理...

高级自定义模板标签包含两部分:

  • 解析模板标签参数的函数
  • 用于模板标签逻辑和输出的Node类

{% try_to_include %}模板标签需要传一个参数,即template_name。因此在 try_to_include() 函数中,我们仅将令牌的分割内容赋值给tag_name变量(try_to_include)及template_name变量。如果出错,则抛出TemplateSyntaxError。该函数返回IncludeNode对象,它获取template_name字段并存储在模板Variable对象中供稍后使用。

在IncludeNode的render()方法中, 我们解析template_name变量。如果模板传递了上下文变量,其值用于template_name。如果将带引号字符串传递给了模板标签,引号中的内容会用于included_template,对应上下文变量的字符串同样会解析到其对等的字符串中。

最后我们将尝试使用解析后的included_template字符串加载模板,使用当前模板上下文对其进行渲染。如果出问题则返回一个空字符串。

至少有两种可以使用模板标签的场景:

  • 包含模型中定义路径的模板时,如下:

    {% load utility_tags %}
    {% try_to_include object.template_path %}
    
  • 包含通过{% with %}模板标签定义路径的模板时,模板标签位于模板上下文变量作用域上方。在对Django CMS中模板占位符中创建插件的自定义布局时尤为有用:

    {# templates/cms/start_page.html #}
    {% load cms_tags %}
    {% with editorial_content_template_path= "cms/plugins/editorial_content/start_page.html" %}
      {% placeholder "main_content" %}
    {% endwith %}
    

稍后,占位符可以通过editorial_content插件进行填充,并且会读取editorial_content_template_path、在可用时安全地包含模板:

{# templates/cms/plugins/editorial_content.html #} 
{% load utility_tags %}
{% if editorial_content_template_path %}
  {% try_to_include editorial_content_template_path %} 
{% else %}
  <div>
    <!-- Some default presentation of editorial content plugin -->
  </div>
{% endif %}

扩展知识...

可以使用{% try_to_include %}标签配合默认的{% include %}标签来包含继承其它模板的模板。对于大规模web平台更有利,可以在各类复杂项列表中共享相同结构为微件,但又使用不同的数据源。

例如,艺术家列表模板中可以包含artist_item模板,如下:

{% load utility_tags %}
{% for object in object_list %}
  {% try_to_include "artists/includes/artist_item.html" %}
{% endfor %}

这一模板会通过基项进行扩展,如下:

{# templates/artists/includes/artist_item.html #}
{% extends "utils/includes/item_base.html" %} 
{% block item_title %}
  {{ object.first_name }} {{ object.last_name }} 
{% endblock %}

基项为每项定义标记并且同时包含Like微件,如下:

{# templates/utils/includes/item_base.html #}
{% load likes_tags %}
<h3>{% block item_title %}{% endblock %}</h3> 
{% if request.user.is_authenticated %}
  {% like_widget for object %}
{% endif %}

相关内容

  • 第4章 模板和JavaScript中的实现Like微件一节
  • 创建模板标签来加载模板中的查询集一节
  • 创建模板标签来解析内容为模板一节
  • 创建模板标签来修改请求查询参数一节

创建模板标签来加载模板中的查询集

通常,网页上显示的内容由视图中的上下文进行定义。如果内容在每页显示,创建一个上下文处理器来让其全局可用会比较合理。另一种情况是在需要显示额外内容时,如最新动态或随机名言,比如在对象的启动页或详情页等页面上。这时可以通过 {% load_objects %}模板标签加载必要的内容,本节中我们就来进行实现。

准备工作

同时使用core应用,预先进行安装并可使用自定义模板标签。

此外,为讲解概念,我们创建一个news应用,其中包含Article模型,如下:

# myproject/apps/news/models.py
from django.db import models
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _

from myproject.apps.core.models import CreationModificationDateBase, UrlBase

class ArticleManager(models.Manager): 
  def random_published(self):
    return self.filter( publishing_status=self.model.PUBLISHING_STATUS_PUBLISHED,).order_by("?")


class Article(CreationModificationDateBase, UrlBase): 
  PUBLISHING_STATUS_DRAFT, PUBLISHING_STATUS_PUBLISHED = "d", "p" 
  PUBLISHING_STATUS_CHOICES = (
    (PUBLISHING_STATUS_DRAFT, _("Draft")),
    (PUBLISHING_STATUS_PUBLISHED, _("Published")),
  )
  title = models.CharField(_("Title"), max_length=200) 
  slug = models.SlugField(_("Slug"), max_length=200)
  content = models.TextField(_("Content")) 
  publishing_status = models.CharField(
    _("Publishing status"), 
    max_length=1, 
    choices=PUBLISHING_STATUS_CHOICES, 
    default=PUBLISHING_STATUS_DRAFT,
  )

  custom_manager = ArticleManager()
  
  class Meta:
    verbose_name = _("Article") 
    verbose_name_plural = _("Articles")
  
  def __str__(self): 
    return self.title
  
  def get_url_path(self):
    return reverse("news:article_detail", kwargs={"slug": self.slug})

有趣的部分是Article模型中的custom_manager。该管理器可用于随机列出所发表的文章。

使用前面一章的示例,我们可以完成带有URL配置、视图、模板和管理后台设置的应用。然后,使用管理后台表单对数据库添加一些文章。

如何实现...

高级自定义模板标签包含解析传递给标签参数的函数以及渲染标签输出或修改模板上下文的Node类。执行如下步骤来创建一个{% load_objects %}模板标签:

  1. 首先,我们来创建处理模板标签参数的函数,如下:

    # myproject/apps/core/templatetags/utility_tags.py
    from django import template 
    from django.apps import apps
    
    register = template.Library() 
    
    """ TAGS """
    
    @register.tag
    def load_objects(parser, token):
      """
      Gets a queryset of objects of the model specified by app and model names
      Usage:
          {% load_objects [<manager>.]<method>
                          from <app_name>.<model_name> 
                          [limit <amount>]
                          as <var_name> %}
      Examples:
          {% load_objects latest_published from people.Person limit 3 as people %} 
          {% load_objects site_objects.all from news.Article as articles %}
          {% load_objects site_objects.all from news.Article limit 3 as articles %}
      """
      limit_count = None 
      try:
        (tag_name, manager_method,
        str_from, app_model,
        str_limit, limit_count,
        str_as, var_name) = token.split_contents()
      except ValueError:
        try:
          (tag_name, manager_method,
          str_from, app_model,
          str_as, var_name) = token.split_contents()
        except ValueError:
          tag_name = token.contents.split()[0] 
          raise template.TemplateSyntaxError(
            f"{tag_name} tag requires the following syntax: " 
            f"{{% {tag_name} [<manager>.]<method> from " 
            "<app_name>.<model_name> [limit <amount>] "
            "as <var_name> %}")
      try:
        app_name, model_name = app_model.split(".")
      except ValueError:
        raise template.TemplateSyntaxError(
          "load_objects tag requires application name "
          "and model name, separated by a dot") 
      model = apps.get_model(app_name, model_name) 
      return ObjectsNode(
        model, manager_method, limit_count, var_name
      )
    
  2. 然后,我们会在同一文件中创建自定义ObjectsNode,继承自基类template.Node。将其插入到load_objects() 函数前,如以下代码所示:

    class ObjectsNode(template.Node):
      def __init__(self, model, manager_method, limit, var_name): 
        self.model = model
        self.manager_method = manager_method
        self.limit = template.Variable(limit) if limit else None 
        self.var_name = var_name
    
      def render(self, context):
        if "." in self.manager_method:
          manager, method = self.manager_method.split(".") 
        else:
          manager = "_default_manager" 
          method = self.manager_method
        
        model_manager = getattr(self.model, manager) 
        fallback_method = self.model._default_manager.none
        qs = getattr(model_manager, method, fallback_method)() 
        limit = None
        if self.limit:
          try:
            limit = self.limit.resolve(context)
          except template.VariableDoesNotExist: 
            limit = None
        context[self.var_name] = qs[:limit] if limit else qs 
        return ""
    
    
    @register.tag
    def load_objects(parser, token):
      #...
    

实现原理...

{% load_objects %}模板标签加载由指定应用和模型中管理器方法所定义的QuerySet,将结果限制为指定数据,并在给定上下文中保存结果。

以下代码是如何使用刚刚创建的模板标签的简单示例。使用如下代码它可以在任意模板中加载所有的新闻资讯:

{% load utility_tags %}
{% load_objects all from news.Article as all_articles %}
<ul>
  {% for article in all_articles %}
    <li><a href="{{ article.get_url_path }}"> {{ article.title }}</a></li>
  {% endfor %}
</ul>

这使用Article模型默认对象管理器的all()方法,并且它会通过模型的Meta类撰写义的ordering属性对文章进行排序。

下面是带数据库中查询对象的自定义方法的自定义管理器的示例。管理器是一个提供对模型进行数据库查询操作的接口。

每个模型默认至少有一个objects管理器。对Article模型,我们增加了一个custom_manager管理器,包含了一个方法random_published()。下面是如何使用它配合{% load_objects %} 模板标签来随机加载一篇已发布文章:

{% load utility_tags %}
{% load_objects custom_manager.random_published from news.Article limit 1 as random_published_articles %}
<ul>
  {% for article in random_published_articles %}
     <li><a href="{{ article.get_url_path }}">
    {{ article.title }}</a></li> 
  {% endfor %}
</ul>

我们来看{% load_objects %} 模板标签的代码。在解析函数中,对该标签允许两种形式-带或不带limit。字符串被进行了解析,如果格式可识别,模板标签的组件会传递给ObjectsNode类。

在Node类的render() 方法中,我们检查了管理器的名称及其方法名。如果未指定管理器,则使用_default_manager。这是一个由Django注册的任意模型的自动化属性,指向第一个可用models.Manager()实例。大部分情况下_default_manager是objects管理器。然后,我们会调用管理器的方法并在方法不存在时使用一个空的QuerySet。如果定义了limit,解析其值并相应地给QuerySet增加限制。最后,在由var_name所指定的上下文变量中存储结果查询集。

相关内容

  • 第2章 模型和数据库结构中的通过URL相关的方法创建模型mixin一节
  • 第2章 模型和数据库结构中的创建模型mixin来处理创建和变更日期一节
  • 创建模板标签在模板存在时包含它一节
  • 创建模板标签来解析内容为模板一节
  • 创建模板标签来修改请求查询参数一节

创建模板标签来解析内容为模板

在本节,我们创建允许在数据库中放置模板代码的 {% parse %} 模板标签。在对已授权和未授权用户提供不同内容、希望包含个性化的问候或不想要在数据库硬编码媒体路径时这会很用。

准备工作

一如往常,我们使用core应用,应先行安装并可定义自定义模板标签。

如何实现...

高级自定义模板标签包含一个解析传递给标签的参数的函数,以及渲染标签输出或修改模板上下文的Node类。执行如下步骤来创建{% parse %} 模板标签:

  1. 首先,创建解析模板标签参数的函数,如下:

    # myproject/apps/core/templatetags/utility_tags.py
    from django import template 
    
    register = template.Library()
    
    """ TAGS """
    
    @register.tag
    def parse(parser, token):
      """
      Parses a value as a template and prints or saves to a variable
    
      Usage:
          {% parse <template_value> [as <variable>] %}
    
      Examples:
        {% parse object.description %}
        {% parse header as header %}
        {% parse "{{ MEDIA_URL }}js/" as js_url %}
      """
      bits = token.split_contents() tag_name = bits.pop(0)
      try:
        template_value = bits.pop(0) 
        var_name = None
        if len(bits) >= 2:
          str_as, var_name = bits[:2] 
      except ValueError:
        raise template.TemplateSyntaxError(
          f"{tag_name} tag requires the following syntax: " 
          f"{{% {tag_name} <template_value> [as <variable>] %}}")
      return ParseNode(template_value, var_name)
    
  2. 然后,我们会在同一文件中创建自定义的ParseNode类,继承自template.Node基类,如以下代码所示(放在parse() 函数之前):

    class ParseNode(template.Node):
      def __init__(self, template_value, var_name): 
        self.template_value = template.Variable(template_value) 
        self.var_name = var_name
      
      def render(self, context):
        template_value = self.template_value.resolve(context) 
        t = template.Template(template_value)
        context_vars = {}
        for d in list(context):
          for var, val in d.items(): 
            context_vars[var] = val
        req_context = template.RequestContext( 
          context["request"], context_vars
        )
        result = t.render(req_context) 
        if self.var_name:
          context[self.var_name] = result
          result = "" 
        return result
    
    @register.tag
    def parse(parser, token):
      #...
    

实现原理...

{% parse %}模板标签让我们可以解析值为模板并立即进行渲染或存储在上下文变量中。

如果我们有一个带有description字段的对象,字段中包含模板变量或逻辑,就可以使用如下代码对其进行解析和渲染:

{% load utility_tags %}
{% parse object.description %}

还可以使用带引号字符串来定义值,如以下代码所示:

{% load static utility_tags %}
{% get_static_prefix as STATIC_URL %}
{% parse "{{ STATIC_URL }}site/img/" as image_directory %} 
<img src="{{ image_directory }}logo.svg" alt="Logo" />

我们来看{% parse %}模板标签的代码。解析函数逐位查看模板标签的参数。首先,我们解析名称和模板值。如果在令牌中还有更多位,我们认为它是一个接在上下文变量名之后的一个可选as单词。模板值及可选变量名传递给了ParseNode类。

该类中的 render()方法首先解析模板变量值并用它创建一个模板对象。会拷贝context_vars并生成一个模板渲染的请求上下文。如果定义了变量名,结果会存储到其中并渲染一个空字符串,否则,所渲染的模板会立即显示。

相关内容

  • 创建模板标签在模板存在时包含它一节
  • 创建模板标签来加载模板中的查询集一节
  • 创建模板标签来修改请求查询参数一节

创建模板标签来修改请求查询参数

Django有一套创建canonical和原始URL的方便灵活的系统,只需要在URL配置文件中添加一些正则表达式规则。但缺少内置的管理查询字符串的技术。像搜索或可过滤对象列表这样的视图需要接收查询参数来细分使用其它参数的过滤结果或进行其它页面。在本节中,我们会创建{% modify_query %}、{% add_to_query %}和 {% remove_from_query %} 模板标签,它让我们可以对当前查询添加、修改或删除参数。

准备工作

老生常谈,我们使用core应用,应将其添加到INSTALLED_APPS中并包含templatetags包。

同时,确保在TEMPLATES设置下对OPTIONS中的context_processors列表添加了请求上下文处理器,如下:

# myproject/settings/_base.py
TEMPLATES = [ 
  {
    "BACKEND": "django.template.backends.django.DjangoTemplates", 
    "DIRS": [os.path.join(BASE_DIR, "myproject", "templates")], 
    "APP_DIRS": True,
    "OPTIONS": {
      "context_processors": [ 
        "django.template.context_processors.debug", 
        "django.template.context_processors.request", 
        "django.contrib.auth.context_processors.auth", 
        "django.contrib.messages.context_processors.messages", 
        "django.template.context_processors.media", 
        "django.template.context_processors.static", 
        "myproject.apps.core.context_processors.website_url",
      ] 
    },
  }
]

如何实现...

对于这些模板标签,我们将使用解析组件的@simple_tag装饰器,并只需要定义渲染函数,如下:

  1. 首先,我们添加帮助方法来汇集每个标签输出的查询字符串:

    # myproject/apps/core/templatetags/utility_tags.py
    from urllib.parse import urlencode
    
    from django import template
    from django.utils.encoding import force_str 
    from django.utils.safestring import mark_safe
    
    register = template.Library()
    
    """ TAGS """
    
    def construct_query_string(context, query_params):
      # empty values will be removed 
      query_string = context["request"].path 
      if len(query_params):
        encoded_params = urlencode([
          (key, force_str(value))
          for (key, value) in query_params if value
        ]).replace("&", "&amp;")
        query_string += f"?{encoded_params}" 
      return mark_safe(query_string)
    
  2. 然后,创建{% modify_query %}模板标签:

    @register.simple_tag(takes_context=True)
    def modify_query(context, *params_to_remove, **params_to_change):
      """Renders a link with modified current query parameters""" 
      query_params = []
      for key, value_list in context["request"].GET.lists():
        if not key in params_to_remove:
          # don't add key-value pairs for params_to_remove 
          if key in params_to_change:
            # update values for keys in params_to_change 
            query_params.append((key, params_to_change[key])) 
            params_to_change.pop(key)
          else:
            # leave existing parameters as they were
            # if not mentioned in the params_to_change
            for value in value_list:
              query_params.append((key, value))
              # attach new params
      for key, value in params_to_change.items():
        query_params.append((key, value))
    
  3. 接着,创建{% add_to_query %}模板标签:

    @register.simple_tag(takes_context=True)
    def add_to_query(context, *params_to_remove, **params_to_add):
      """Renders a link with modified current query parameters""" 
      query_params = []
      # go through current query params..
      for key, value_list in context["request"].GET.lists():
        if key not in params_to_remove:
          # don't add key-value pairs which already 
          # exist in the query
          if (key in params_to_add and params_to_add[key] in value_list): 
            params_to_add.pop(key)
          for value in value_list: 
            query_params.append((key, value))
      # add the rest key-value pairs
      for key, value in params_to_add.items():
        query_params.append((key, value))
      return construct_query_string(context, query_params)
    
  4. 最后,创建{% remove_from_query %}模板标签:

    @register.simple_tag(takes_context=True)
    def remove_from_query(context, *args, **kwargs):
      """Renders a link with modified current query parameters""" 
      query_params = []
      # go through current query params..
      for key, value_list in context["request"].GET.lists():
        # skip keys mentioned in the args
        if key not in args:
          for value in value_list:
            # skip key-value pairs mentioned in kwargs 
            if not (key in kwargs and str(value) == str(kwargs[key])): 
              query_params.append((key, value))
      return construct_query_string(context, query_params)
    

实现原理...

所创建的三个模板标签类似。首先,它们从类似字典的QueryDict对象request.GE读取当前查询参数,该对象是对(key, value) query_params元组新列表的。然后根据位置参数和关键字参数更新值。最后,通过一开始定义的帮助方法形成新的查询字符串。在这一过程中,所有空格和特殊字符都进行了URL编码,连接查询字符串的&符也进行了转义。新的查询字符串被返回给了模板。

ℹ️阅读更多有关QueryDict对象,参见Django官方文档

我们来看使用{% modify_query %}模板标签的示例。模板标签中的位置参数定义要删除哪个查询参数,关键字参数定义在当前查询中更新哪个查询参数。如果当前URL是http://127.0.0.1:8000/artists/?category=fine-art&page=5 ,我们可以使用如下模板标签来渲染进入下一页的模板标签:

{% load utility_tags %}
<a href="{% modify_query page=6 %}">6</a>

以下是使用上面的模板标签所渲染的输出代码:

<a href="/artists/?category=fine-art&amp;page=6">6</a>

我们还可以使用如下示例来渲染重置页码的链接进入另一个分类sculpture,如下:

{% load utility_tags %}
<a href="{% modify_query "page" category="sculpture" %}">
  Sculpture
</a>

因此,使用以上模板标签所渲染的输入如下所示:

<a href="/artists/?category=sculpture"> 
  Sculpture
</a>

通过{% add_to_query %}模板标签,可以通过同一名称逐步添加查询参数。例如,如果当前URL为http://127.0.0.1:8000/artists/?category=fine-art ,可以借助如下代码添加另一个分类Sculpture:

{% load utility_tags %}
<a href="{% add_to_query category="sculpture" %}">
  + Sculpture
</a>

这会在模板中进行渲染,如以下代码所示:

<a href="/artists/?category=fine-art&amp;category=sculpture"> 
  + Sculpture
</a>

最后,借助{% remove_from_query %} 模板标签,可以使用相同名称逐步删除查询参数。例如当前URL为http://127.0.0.1:8000/artists/?category=fine-art&category=sculpture ,可以借助如下代码删除Sculpture分类:

{% load utility_tags %}
<a href="{% remove_from_query category="sculpture" %}">
  - Sculpture 
</a>

这会在模板中进行如下渲染:

<a href="/artists/?category=fine-art"> 
  - Sculpture
</a>

相关内容

  • 第3章 表单和视图中的过滤对象列表一节
  • 创建模板标签在模板存在时包含它一节
  • 创建模板标签来加载模板中的查询集一节
  • 创建模板标签来解析内容为模板一节