自動轉義HTML
當從模版中生成HTML檔案時,總會存在各種風險,比如xss代碼注入等惡意攻擊,比如下面的模版片段:
Hello, {{ name }}
首先,它看起來像是無害的,用來顯示用戶的名字,但是設想一下,如果用戶像下面這樣輸入他的名字,會發生什么:
<script>alert('hello')</script>
使用這個名字的值,模版將會被渲染成這樣:
Hello, <script>alert('hello')</script>
這意味著瀏覽器會彈出一個JavaScript警報框!
顯然,用戶提交的資料都被不應該被盲目的信任,并且被直接插入到網頁中,因為一個懷有惡意的用戶可能會使用這樣的漏洞來做一些壞事, 這種型別的安全問題被叫做跨站腳本攻擊(Cross Site Scripting)(XSS),
為避免這個問題,有兩個選擇:
- 第一,對每個不被信任的值運行escape過濾器,這將把潛在的有害的HTML字符轉換成無害的字串,在Django最初的幾年里,這是默認的解決方案,但問題是它將責任放在開發人員/模板作者身上,以確保轉義了所有內容,而且很容易忘記轉義資料,
- 第二,利用Django的自動HTML轉義功能,默認情況下,Django中的每個模板會自動轉義每個變數,也就是說,下面五個字符將被轉義:
- <會轉換為<
- >會轉換為>
- '(單引號)轉換為'
- "(雙引號)會轉換為"
- &會轉換為&
建議:將第二種功能做為默認打開的設定,
但是,有時,模板變數含有一些你打算渲染成原始HTML的資料,你并不想轉義這些內容, 例如,你可能會在資料庫中儲存一些HTML代碼,并且直接在模板中嵌入它們,有以下解決方法:
對于單個變數:
使用safe過濾器來關閉變數上的自動轉義:
This will be escaped: {{ data }}
This will not be escaped: {{ data|safe }}
對于模板塊:
要控制模板上的自動轉義,將模板(或者模板中的特定區域)包裹在autoescape標簽中,像這樣:
{% autoescape off %}
Hello {{ name }}
{% endautoescape %}
autoescape標簽接受on或者off作為它的引數,下面是一個模板的示例:
Auto-escaping is on by default. Hello {{ name }}
{% autoescape off %}
This will not be auto-escaped: {{ data }}.
Nor this: {{ other_data }}
{% autoescape on %}
Auto-escaping applies again: {{ name }}
{% endautoescape %}
{% endautoescape %}
自動轉義標簽autoescape還會作用于擴展(extend)了當前模板的模板,以及通過include標簽包含的模板,就像所有block標簽那樣, 看下面的例子:
# base.html檔案
{% autoescape off %}
<h1>{% block title %}{% endblock %}</h1>
{% block content %}
{% endblock %}
{% endautoescape %}
# child.html檔案
{% extends "base.html" %}
{% block title %}This & that{% endblock %}
{% block content %}{{ greeting }}{% endblock %}
由于自動轉義標簽在base模板中關閉,它也會在child模板中關閉,導致當greeting變數含有Hello!字串時,會渲染HTML,
過濾器的字串引數:
過濾器的引數可以是字串:
{{ data|default:"This is a string literal." }}
要注意,所有這種字串引數在插入模板時都不會進行任何自動轉義,模板的作者可以控制字串字面值的內容,可以確保在模板撰寫時文本經過正確轉義,即對自己傳遞的引數心里要有數,
{{ data|default:"3 < 2" }} # 正確的做法
{{ data|default:"3 < 2" }} # 錯誤的做法
方法呼叫
大多數物件上的方法呼叫同樣可用于模板中,這意味著模板能夠訪問到的不僅僅是物件的屬性(比如欄位名稱)和視圖中傳入的變數,還可以執行物件的方法,例如:QuerySets提供了count()方法來計算含有物件的總數,因此,你可以像這樣獲取所有關于當前任務的評論總數:
{{ task.comment_set.all.count }}
還可以訪問已經顯式定義在模型上的方法:
# models.py
class Task(models.Model):
def foo(self):
return "bar"
template.html
{{ task.foo }}
由于Django有意限制了模板語言中的處理邏輯,不能夠在模板中傳遞引數來呼叫方法,資料應該在視圖中處理,然后傳遞給模板用于展示,
多對多呼叫
對于如下的模型:
from django.db import models
# Create your models here.
class Student(models.Model):
name = models.CharField(max_length=128)
class Course(models.Model):
name = models.CharField(max_length=128)
students = models.ManyToManyField('Student')
模型Course有一個多對多欄位指向Student模型,
正向查詢
假設撰寫了一個如下的視圖:
def test(request):
course = models.Course.objects.get(pk=1) #pk:有主鍵的欄位
return render(request, 'course.html', locals())
獲取了id為1的course物件,并將它傳遞給course.html模版,模版代碼如下:
{% for student in course.students.all %}
<p>{{ student.name }}</p>
{% endfor %}
首先通過course.students.all,查尋到course物件關聯的students物件集,然后用for標簽回圈它,獲取每個student物件,再用student模型的定義,訪問其各個欄位的屬性,
反向查詢
對于反向查詢,從student往course查,假設有如下的視圖:
def test2(request):
student = models.Student.objects.get(pk=1)
return render(request, 'student.html', locals())
獲取了id為1的student物件,并將它傳遞給student.html模版,模版代碼如下:
{% for course in student.course_set.all %}
{{ course.name }}
{% endfor %}
通過student.course_set.all,反向獲取到student實體對應的所有course物件,然后再for標簽回圈每個course,呼叫course的各種欄位屬性,
對于外鍵ForeignKey,其用法基本類似,只不過正向是obj.fk,且只有1個對像,不是集合,反向則是obj.fk_set,類似多對多,
定義標簽和過濾器
Django為我們提供了自定義的機制,可以通過使用Python代碼,自定義標簽和過濾器來擴展模板引擎,然后使用{% load %}標簽,
前置步驟
Django對于自定義標簽和過濾器是有前置要求的,首先一條就是代碼布局和檔案組織:你可以為你的自定義標簽和過濾器新開一個app,也可以在原有的某個app中添加,
在app中新建一個
templatetags包(名字固定,不能變),和views.py、models.py等檔案處于同一級別目錄下,不要忘記創建__init__.py檔案以使得該目錄可以作為Python的包,在添加templatetags包后,需要重新啟動服務器,然后才能在模板中使用標簽或過濾器,
- 將你自定義的標簽和過濾器將放在templatetags包下的一個模塊里,
- 這個模塊的名字是后面載入標簽時使用的標簽名,所以要謹慎的選擇名字以防與Django內置的或其他應用下的(自定義)標簽和過濾器名字沖突,
假設你自定義的標簽/過濾器在一個名為poll_extras.py的檔案中:
polls/
__init__.py
models.py
templatetags/
__init__.py
poll_extras.py
views.py
為了讓{% load xxx %}標簽正常作業,包含自定義標簽的app必須在INSTALLED_APPS中注冊,然后就可以在模板中像如下這樣使用:
{% load poll_extras %}
在templatetags包中放多少個模塊沒有限制,{% load xxx %}將會載入給定模塊名中的標簽/過濾器,而不是app中所有的標簽和過濾器,
要在模塊內自定義標簽,首先,這個模塊必須包含一個名為register的變數,它是template.Library的一個實體,所有的標簽和過濾器都是在其中注冊的, 所以把如下的內容放在你的模塊的頂部:
from django import template
register = template.Library()
友情提示:可以閱讀Django的默認過濾器和標記的源代碼,它們分別位于django/template/defaultfilters.py和django/template/defaulttags.py中,它們是最好的范例,
自定義過濾器
自定義過濾器就是一個帶有一個或兩個引數的Python函式,這個Python函式的第一個引數要過濾的物件,第二個引數才是自定義的引數,而且最多總共只能有兩個引數,所以你只能自定義一個引數,
- 變數的值:不一定是字串形式,
- 引數的值:可以有一個初始值,或者完全不要這個引數,
由于模板語言沒有提供例外處理,任何從過濾器中拋出的例外都將會顯示為服務器錯誤,
下面是一個定義過濾器的例子:
def cut(value, arg):
"""將value中的所有arg部分切除掉"""
return value.replace(arg, '')
下面是這個過濾器的使用方法:
{{ somevariable|cut:"0" }}
大多數過濾器沒有引數,但基本的value引數是必帶的,例如:
def lower(value): # Only one argument.
"""Converts a string into all lowercase"""
return value.lower()
注冊過濾器
類原型:django.template.Library.filter()
好了過濾器函式,就需要注冊它,有兩種方法:
- 方法是呼叫
register.filter,比如:
register.filter('cut', cut)
register.filter('lower', lower)
Library.filter()方法需要兩個引數:
- 過濾器的名稱:一個字串物件
- 編譯的函式 :撰寫的過濾器函式
- 把
register.filter()用作裝飾器,以如下的方式注冊過濾器:
@register.filter(name='cut')
def cut(value, arg):
return value.replace(arg, '')
@register.filter #沒有宣告name引數,Django將使用函式名作為過濾器的名字,
def lower(value):
return value.lower()
自定義標簽
Django提供了大量的快捷方式,使得撰寫標簽比較容易, 對于一般的自定義標簽來說,simple_tag是最重要的,它幫助你將一個Python函式注冊為一個簡單的模版標簽,
simple_tag
原型:django.template.Library.simple_tag()
為了簡單化模版標簽的創建,Django提供一個輔助函式simple_tag,這個函式是django.template.Library的一個方法,
具體的撰寫方法:
在settings中的INSTALLED_APPS配置當前app,不然django無法找到自定義的simple_tag,
在app中創建templatetags模塊(模塊名只能是templatetags),
創建任意 .py 檔案,如:my_tags.py
在使用自定義simple_tag和filter的html檔案中匯入之前創建的 my_tags.py,
{% load my_tags %}
from django import template
register = template.Library() #register的名字是固定的,不可改變
@register.filter
def filter_multi(v1,v2):
return v1 * v2
@register.simple_tag # 和自定義filter類似,只不過接收更靈活的引數,沒有個數限制,
def simple_tag_multi(v1,v2):
return v1 * v2
呼叫:
-------------------------------.html
{% load xxx %}
# num=12
{{ num|filter_multi:2 }} # 24
{% simple_tag_multi num 5 %} # 引數不限,但不能放在if、for陳述句中
注意:filter可以用在if、for等陳述句后,simple_tag不可以
{% if num|filter_multi:30 > 100 %}
{{ num|filter_multi:30 }}
{% endif %}
比如,我們想撰寫一個回傳當前時間的模版標簽,那么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函式有幾件值得注意的事項︰
如果不需要額外的轉義,可以使用mark_safe()讓輸出不進行轉義,前提是你絕對確保代碼中不包含XSS漏洞, 如果要創建小型HTML片段,強烈建議使用format_html()而不是mark_safe(),
from django import template
from django.utils.safestring import mark_safe
register = template.Library() #register的名字是固定的,不可改變
@register.simple_tag
def my_input(id,arg):
result = "<input type='text' id='%s' class='%s' />" %(id,arg,)
return mark_safe(result)
如果模板標簽需要訪問當前背景關系,可以在注冊標簽時使用takes_context引數︰
@register.simple_tag(takes_context=True)
def current_time(context, format_string): # 一個引數必須稱作context
timezone = context['timezone']
return your_get_current_time_method(timezone, format_string)
如果你需要重命名你的標簽,可以給它提供自定義的名稱︰
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 ...
然后在模板中,可以將任意數量的由空格分隔的引數傳遞給模板標簽,關鍵字引數的值使用等號("=")賦予,并且必須在位置引數之后提供, 例子:
{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}
可以將標簽結果存盤在模板變數中,而不是直接輸出,這是通過使用as引數后跟變數名來實作的:
{% current_time "%Y-%m-%d %I:%M %p" as the_time %}
<p>The time is {{ the_time }}.</p>
渲染組件
inclusion_tag
原型:django.template.Library.inclusion_tag()
另一種常見型別的模板標簽是通過渲染一個模板來顯示一些資料,例如,Django的Admin界面使用自定義模板標簽顯示"添加/更改"表單頁面底部的按鈕,這些按鈕看起來總是相同,但鏈接的目標卻是根據正在編輯的物件而變化的,這種型別的標簽被稱為"Inclusion 標簽",多用于回傳html代碼片段,
例:
#templatetags/inclusion_tag_test.py
from django import template
register = template.Library()
@register.inclusion_tag('result.html') #將result.html里面的內容用下面函式的回傳值渲染,然后作為一個組件一樣,加載到使用這個函式的html檔案里面,
def show_results(n): # 引數可以傳多個進來
n = 1 if n < 1 else int(n)
data = ["第{}項".format(i) for i in range(1, n+1)]
return {"data": data} # 這里可以傳多個值,
# templates/snippets/result.html
<ul>
{% for choice in data %}
<li>{{ choice }}</li>
{% endfor %}
</ul>
# templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="x-ua-compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>inclusion_tag test</title>
</head>
<body>
{% load inclusion_tag_test %}
{% show_results 10 %}
</body>
</html>
注冊有兩種方法:
# 通過呼叫Library物件的inclusion_tag()裝飾器方法創建并注冊Inclusion標簽︰
@register.inclusion_tag('results.html') # results.html要渲染的html檔案,
def show_results(poll):
...
# 也可以不用裝飾器方法:django.template.Template實體注冊Inclusion標簽︰
from django.template.loader import get_template
t = get_template('results.html')
register.inclusion_tag(t)(show_results)
inclusion_tag函式可以接受任意數量的位置引數和關鍵字引數,像這樣:
@register.inclusion_tag('my_template.html')
def my_tag(a, b, *args, **kwargs):
warning = kwargs['warning']
profile = kwargs['profile']
...
return ...
然后在模板中,可以將任意數量的由空格分隔的引數傳遞給模板標簽,關鍵字引數的值的設定使用等號("=") ,并且必須在位置引數之后提供,例如:
{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}
可以在標簽中傳遞背景關系中的引數,比如說,當你想要將背景關系context中的home_link和home_title這兩個變數傳遞給模版, 如下所示:
@register.inclusion_tag('link.html', takes_context=True)
def jump_link(context):
return {
'link': context['home_link'],
'title': context['home_title'],
}
注意函式的第一個引數必須叫做context,context必須是一個字典型別,
在register.inclusion_tag()這一行,我們指定了takes_context=True和模板的名字,模板link.html很簡單,如下所示:
Jump directly to <a href=https://www.cnblogs.com/wby-110/p/"{{ link }}">{{ title }}</a>.
然后,當任何時候你想呼叫這個自定義的標簽時,只需要load它本身,不需要添加任何引數,{{ link }}和{{ title }}會自動從標簽中獲取引數值,像這樣:
{% jump_link %}
使用takes_context=True,就表示不需要傳遞引數給這個模板標簽,它會自己去獲取背景關系,
或者使用django.template.Template實體注冊Inclusion標簽︰
from django.template.loader import get_template
t = get_template('results.html')
register.inclusion_tag(t)(show_results)
使用自定義標簽和過濾器
某些應用提供了自定義的標簽和過濾器,想要在模板中使用它們,首先要確保該應用已經在INSTALLED_APPS 中(比如在下面的例子中,添加了'django.contrib.humanize'),之后在模板中使用load標簽:
{% load humanize %}
{{ 45000|intcomma }}
上面的例子中, load標簽加載了humanizeapp的標簽庫,之后我們可以使用它的intcomma過濾器,
開啟django.contrib.admindocs查詢admin站點中的檔案,便可以查看安裝的自定義庫串列,
load標簽可以同時接受多個庫名稱,由空格分隔, 例如:
{% load humanize i18n %}
自定義庫和模板繼承:
當你加載一個自定義標簽或過濾器庫時,標簽或過濾器只在當前模板中有效--并不是帶有模板繼承關系的任何父模板或者子模版中都有效,即在父模板中加載了自定義標簽,子模版中還要再加載一次,
例如,如果一個模板foo.html帶有{% load humanize %},子模版(例如,帶有{% extends "foo.html" %})中不能訪問humanize模板標簽和過濾器, 子模版需要再添加自己的{% load humanize %},
這個特性是出于保持可維護性和邏輯性的目的,
配置模板引擎
模板引擎通過settings中的TEMPLATES設定來配置,串列中每個元素都是一個引擎配置字典,由startproject命令生成的settings.py會自定定義如下的值:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
# ... some options here ...
},
},
]
# BACKEND:后端,內置的后端有django.template.backends.django.DjangoTemplates和django.template.backends.jinja2.Jinja2,
OPTIONS中包含了具體的后端設定,
由于絕大多數引擎都是從檔案加載模板的,所以每種模板引擎都包含兩項通用設定:
- DIRS:定義了一個目錄串列,模板引擎按串列順序搜索這些目錄以查找模板源檔案,
- APP_DIRS:告訴模板引擎是否應該進入每個已安裝的應用中查找模板,通常將該選項保持為True,
每種模板引擎后端都定義了一個慣用的名稱作為應用內部存放模板的子目錄名稱,(例如Django為它自己的模板引擎指定的是 ‘templates’,為jinja2指定的名字是‘jinja2’),尤其是,django允許你有多個模板引擎后端實體,且每個實體有不同的配置選項, 在這種情況下必須為每個配置指定一個唯一的NAME ,
DTL引擎的OPTIONS配置項中接受以下引數:
autoescape:一個布林值,用于控制是否啟用HTML自動轉義功能,默認為True,context_processors: 以"."為分隔符的Python呼叫路徑的串列,默認是個空串列,debug:打開/關閉模板除錯模式的布林值,默認和setting中的DEBUG有相同的值,loaders:模板加載器類的虛擬Python路徑串列,默認值取決于DIRS和APP_DIRS的值,string_if_invalid:非法變數時輸出的字串,默認為空字串,file_charset:用于讀取磁盤上的模板檔案的字符集編碼,默認為FILE_CHARSET的值,libraries:用于注冊模板引擎, 這可以用于添加新的庫或為現有庫添加備用標簽,builtins:以圓點分隔的Python路徑的串列,
簡單的用法
django.template.loader中定義了兩個函式以加載模板,
get_template(template_name,using = None)[source]
該函式使用給定的名稱查找和加載模板,并回傳一個Template物件,
模板的查找和加載機制取決于每種后端引擎和配置,如果想使用指定的模板引擎進行查找,請將模板引擎的NAME賦給get_template的using引數,
select_template(template_name_list,using = None)[source]
和get_template()相似, 只不過它使用包含模板名稱的串列作為引數,
由select_template()和get_template()回傳的Template物件都必須提供一個render()方法,如下所示:
Template.render(context=None, request=None)
通過給定的context對該模板進行渲染,
如果提供了context,那么它必須是一個dict物件,如果要提供request引數 ,必須使用HttpRequest物件,
針對下面的TEMPLATES配置,對模版檔案的搜索順序和路徑如下:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
'/home/html/example.com',
'/home/html/default',
],
},
{
'BACKEND': 'django.template.backends.jinja2.Jinja2',
'DIRS': [
'/home/html/jinja2',
],
},
]
如果你呼叫函式get_template('story_detail.html'), Django將按以下順序查找story_detail.html:
/home/html/example.com/story_detail.html('django'引擎)
/home/html/default/story_detail.html('django'引擎)
/home/html/jinja2/story_detail.html('jinja2'引擎)
如果你呼叫函式select_template(['story_253_detail.html','story_detail.html']),Django按以下順序查找:
/home/html/example.com/story_253_detail.html('django'引擎)
/home/html/default/story_253_detail.html('django'引擎)
/home/html/jinja2/story_253_detail.html('jinja2'引擎)
/home/html/example.com/story_detail.html('django'引擎)
/home/html/default/story_detail.html('django'引擎)
/home/html/jinja2/story_detail.html('jinja2'引擎)
注意:Django查找到任何一個匹配的模板后便停止搜尋,所以這是個類似url搜索的短路操作!
建議在每個APP的的模版子目錄下都建立一個子目錄來唯一對應這個APP,這樣做可以增強APP的可用性, 將所有的模版檔案放在根模版目錄下會引發混淆,
要在一個子目錄內加載模板,像下面這樣:
get_template('news/story_detail.html')
如果結合上面例子中的TEMPLATES配置,這將會嘗試按順序查找并加載下列模板︰
/home/html/example.com/news/story_detail.html('django'引擎)
/home/html/default/news/story_detail.html('django'引擎)
/home/html/jinja2/news/story_detail.html('jinja2'引擎)
另外,為了減少加載模板、渲染模板等重復作業,django提供了處理這些作業的快捷函式,
render_to_string(template_name, context=None, request=None, using=None)[source]
render_to_string()會像get_template()一樣加載模板并立即呼叫render()方法, 它需要以下引數,
- TEMPLATE_NAME:要加載的模板的名稱或串列,
- context:要用作模板的背景關系進行渲染的資料字典,也就是你要插入的動態資料字典,
- request:可選的HttpRequest物件,
- using:指定使用的模板引擎NAME, 搜索模板將僅限于該引擎,
用法示例:
from django.template.loader import render_to_string
rendered = render_to_string('my_template.html', {'foo': 'bar'})
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/79396.html
標籤:Python
上一篇:Git入門
