Så här skapar du anpassade malltaggar och filter¶
Djangos mallspråk levereras med ett brett utbud av inbyggda taggar och filter som är utformade för att tillgodose presentationslogikbehoven i din applikation. Ändå kan det hända att du behöver funktionalitet som inte täcks av kärnuppsättningen av mallprimitiver. Du kan utöka mallmotorn genom att definiera anpassade taggar och filter med Python och sedan göra dem tillgängliga för dina mallar med hjälp av taggen {% load %}
.
Layout för kod¶
Den vanligaste platsen för att ange anpassade malltaggar och filter är inuti en Django-app. Om de relaterar till en befintlig app är det vettigt att samla dem där; annars kan de läggas till i en ny app. När en Django-app läggs till i INSTALLED_APPS
, görs alla taggar som den definierar på den konventionella plats som beskrivs nedan automatiskt tillgängliga för laddning i mallar.
Appen bör innehålla en katalog med namnet templatetags
, på samma nivå som models.py
, views.py
osv. Om den inte redan finns, skapa den - glöm inte filen __init__.py
för att se till att katalogen behandlas som ett Python-paket.
Utvecklingsservern startar inte om automatiskt
När du har lagt till modulen templatetags
måste du starta om servern innan du kan använda taggarna eller filtren i mallarna.
Dina anpassade taggar och filter kommer att finnas i en modul i katalogen templatetags
. Namnet på modulfilen är det namn som du använder för att ladda taggarna senare, så var noga med att välja ett namn som inte krockar med anpassade taggar och filter i en annan app.
Om dina anpassade taggar/filter till exempel finns i en fil som heter poll_extras.py
, kan din applayout se ut så här:
polls/
__init__.py
models.py
templatetags/
__init__.py
poll_extras.py
views.py
Och i din mall skulle du använda följande:
{% load poll_extras %}
Appen som innehåller de anpassade taggarna måste finnas i INSTALLED_APPS
för att taggen {% load %}
ska fungera. Detta är en säkerhetsfunktion: Det gör att du kan vara värd för Python-kod för många mallbibliotek på en enda värdmaskin utan att aktivera åtkomst till dem alla för varje Django-installation.
Det finns ingen gräns för hur många moduler du lägger in i paketet templatetags
. Tänk bara på att en {% load %}
-sats kommer att ladda taggar/filter för det angivna Python-modulnamnet, inte namnet på appen.
För att vara ett giltigt taggbibliotek måste modulen innehålla en variabel på modulnivå som heter register
som är en template.Library
-instans, där alla taggar och filter registreras. Så, nära toppen av din modul, lägg till följande:
from django import template
register = template.Library()
Alternativt kan malltaggmoduler registreras via argumentet 'libraries'
till DjangoTemplates
. Detta är användbart om du vill använda en annan etikett än namnet på template-taggmodulen när du laddar template-taggar. Det gör det också möjligt för dig att registrera taggar utan att installera en applikation.
Bakom kulisserna
För massor av exempel, läs källkoden för Djangos standardfilter och taggar. De finns i django/template/defaultfilters.py respektive django/template/defaulttags.py.
För mer information om taggen load
, läs dess dokumentation.
Skriva anpassade mallfilter¶
Anpassade filter är Python-funktioner som tar ett eller två argument:
Värdet på variabeln (input) - inte nödvändigtvis en sträng.
Argumentets värde - detta kan ha ett standardvärde eller utelämnas helt och hållet.
I filtret {{ var|foo:"bar" }}
får till exempel filtret foo
variabeln var
och argumentet "bar"
.
Eftersom mallspråket inte tillhandahåller undantagshantering kommer alla undantag som skapas av ett mallfilter att exponeras som ett serverfel. Därför bör filterfunktioner undvika att skapa undantag om det finns ett rimligt fallback-värde att returnera. När det gäller indata som representerar en tydlig bugg i en mall kan det fortfarande vara bättre att skapa ett undantag än ett tyst misslyckande som döljer buggen.
Här är ett exempel på en filterdefinition:
def cut(value, arg):
"""Removes all values of arg from the given string"""
return value.replace(arg, "")
Och här är ett exempel på hur det filtret skulle kunna användas:
{{ somevariable|cut:"0" }}
De flesta filter tar inte emot argument. I detta fall, lämna argumentet utanför din funktion:
def lower(value): # Only one argument.
"""Converts a string into all lowercase"""
return value.lower()
Registrering av anpassade filter¶
- django.template.Library.filter()¶
När du har skrivit din filterdefinition måste du registrera den med din Library
-instans för att göra den tillgänglig för Djangos mallspråk:
register.filter("cut", cut)
register.filter("lower", lower)
Metoden Library.filter()
tar två argument:
Filtrets namn - en sträng.
Kompileringsfunktionen – en Python-funktion (inte namnet på funktionen som en sträng).
Du kan använda register.filter()
som en dekorator istället:
@register.filter(name="cut")
def cut(value, arg):
return value.replace(arg, "")
@register.filter
def lower(value):
return value.lower()
Om du utelämnar argumentet name
, som i det andra exemplet ovan, kommer Django att använda funktionens namn som filternamn.
Slutligen accepterar register.filter()
också tre nyckelordsargument, is_safe
, needs_autoescape
och expects_localtime
. Dessa argument beskrivs i filter och auto-escaping och filter och tidszoner nedan.
Mallfilter som förväntar sig strängar¶
- django.template.defaultfilters.stringfilter()¶
Om du skriver ett mallfilter som bara förväntar sig en sträng som första argument, bör du använda dekoratorn stringfilter
. Detta kommer att konvertera ett objekt till dess strängvärde innan det skickas till din funktion:
from django import template
from django.template.defaultfilters import stringfilter
register = template.Library()
@register.filter
@stringfilter
def lower(value):
return value.lower()
På så sätt kan du skicka t.ex. ett heltal till detta filter utan att det orsakar ett AttributeError
(eftersom heltal inte har lower()
-metoder).
Filter och automatisk särskrivning¶
När du skriver ett anpassat filter, tänk på hur filtret kommer att interagera med Djangos auto-escaping-beteende. Observera att två typer av strängar kan skickas runt inuti mallkoden:
Raw strings är de ursprungliga Python-strängarna. Vid utmatning escapas de om automatisk escaping är i bruk och presenteras annars oförändrade.
Säkra strängar är strängar som har markerats som säkra från ytterligare escaping vid utmatningstillfället. All nödvändig escaping har redan gjorts. De används ofta för utdata som innehåller rå HTML som är avsedd att tolkas som den är på klientsidan.
Internt är dessa strängar av typen
SafeString
. Du kan testa för dem med hjälp av kod som:from django.utils.safestring import SafeString if isinstance(value, SafeString): # Do something with the "safe" string. ...
Mallfilterkod faller i en av två situationer:
Ditt filter introducerar inte några HTML-osäkra tecken (
<
,>
,'
,"
eller&
) i resultatet som inte redan fanns. I det här fallet kan du låta Django ta hand om all hantering av automatisk eskapning åt dig. Allt du behöver göra är att ställa in flagganis_safe
tillTrue
när du registrerar din filterfunktion, så här:@register.filter(is_safe=True) def myfilter(value): return value
Denna flagga talar om för Django att om en ”säker” sträng skickas in i ditt filter, kommer resultatet fortfarande att vara ”säkert” och om en icke-säker sträng skickas in, kommer Django automatiskt att undkomma den, om det behövs.
Du kan tänka dig att detta betyder ”det här filtret är säkert - det introducerar inte någon möjlighet till osäker HTML”
Anledningen till att
is_safe
är nödvändig är att det finns gott om normala strängoperationer som kommer att göra ettSafeData
-objekt tillbaka till ett normaltstr
-objekt och i stället för att försöka fånga dem alla, vilket skulle vara mycket svårt, reparerar Django skadan efter att filtret har slutförts.Anta till exempel att du har ett filter som lägger till strängen
xx
i slutet av alla inmatningar. Eftersom detta inte tillför några farliga HTML-tecken till resultatet (förutom de som redan fanns där), bör du markera ditt filter medis_safe
:@register.filter(is_safe=True) def add_xx(value): return "%sxx" % value
När detta filter används i en mall där automatisk escaping är aktiverad, kommer Django att escapa utdata när indata inte redan är markerad som ”säker”.
Som standard är
is_safe
False
, och du kan utelämna det från alla filter där det inte krävs.Var försiktig när du avgör om ditt filter verkligen lämnar säkra strängar som säkra. Om du tar bort tecken kan du oavsiktligt lämna obalanserade HTML-taggar eller enheter i resultatet. Om du till exempel tar bort en
>
från indata kan<a>
bli<a
, som måste escapas i utdata för att inte orsaka problem. På samma sätt kan borttagning av ett semikolon (;
) förvandla&
till&amp
, som inte längre är en giltig enhet och därför behöver ytterligare escaping. De flesta fall kommer inte att vara så här knepiga, men håll utkik efter sådana problem när du granskar din kod.Om du markerar ett filter med
is_safe
kommer filtrets returvärde att tvingas till en sträng. Om ditt filter ska returnera ett booleskt värde eller ett annat värde som inte är en sträng, kommer markeringenis_safe
förmodligen att få oavsiktliga konsekvenser (t.ex. konvertering av ett booleskt värde False till strängen ’False’).Alternativt kan din filterkod manuellt ta hand om all nödvändig escaping. Detta är nödvändigt när du introducerar ny HTML-markup i resultatet. Du vill markera utdata som säker från ytterligare escaping så att din HTML-markering inte escapas ytterligare, så du måste hantera inmatningen själv.
För att markera utdata som en säker sträng, använd
django.utils.safestring.mark_safe()
.Var dock försiktig. Du behöver göra mer än att bara markera utdata som säker. Du måste se till att det verkligen är säkert, och vad du gör beror på om auto-escaping är i kraft. Tanken är att skriva filter som kan fungera i mallar där auto-escaping är antingen på eller av för att göra saker enklare för dina mallförfattare.
För att ditt filter ska känna till det aktuella auto-escaping-läget ska du ställa in flaggan
needs_autoescape
tillTrue
när du registrerar din filterfunktion. (Om du inte anger den här flaggan är standardvärdetFalse
). Denna flagga talar om för Django att din filterfunktion vill få ett extra nyckelordsargument, kallatautoescape
, som ärTrue
om auto-escaping är i kraft ochFalse
annars. Det rekommenderas att ställa in standardvärdet för parameternautoescape
tillTrue
, så att om du anropar funktionen från Python-kod kommer den att ha escaping aktiverad som standard.Låt oss t.ex. skriva ett filter som betonar det första tecknet i en sträng:
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=True): 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)
Flaggan
needs_autoescape
och nyckelordsargumentetautoescape
innebär att vår funktion kommer att veta om automatisk escaping är i kraft när filtret anropas. Vi använderautoescape
för att avgöra om indata behöver skickas genomdjango.utils.html.conditional_escape
eller inte. (I det senare fallet använder vi identitetsfunktionen som ”escape”-funktion.) Funktionenconditional_escape()
är somescape()
förutom att den bara escaper indata som inte är enSafeData
-instans. Om enSafeData
-instans skickas tillconditional_escape()
returneras data oförändrat.Slutligen, i exemplet ovan, kommer vi ihåg att markera resultatet som säkert så att vår HTML infogas direkt i mallen utan ytterligare escaping.
Det finns ingen anledning att oroa sig för flaggan
is_safe
i det här fallet (även om det inte skulle skada något att inkludera den). När du manuellt hanterar auto-escaping-problemen och returnerar en säker sträng, kommeris_safe
-flaggan inte att ändra någonting på något sätt.
Varning
Undvik XSS-sårbarheter när du återanvänder inbyggda filter
Djangos inbyggda filter har autoescape=True
som standard för att få rätt autoescaping-beteende och undvika en cross-site script-sårbarhet.
I äldre versioner av Django bör du vara försiktig när du återanvänder Djangos inbyggda filter eftersom autoescape
som standard är None
. Du måste skicka autoescape=True
för att få autoescaping.
Om du till exempel vill skriva ett anpassat filter som heter urlize_and_linebreaks
och som kombinerar filtren urlize`
och linebreaksbr`
, skulle filtret se ut på följande sätt:
from django.template.defaultfilters import linebreaksbr, urlize
@register.filter(needs_autoescape=True)
def urlize_and_linebreaks(text, autoescape=True):
return linebreaksbr(urlize(text, autoescape=autoescape), autoescape=autoescape)
Då så:
{{ comment|urlize_and_linebreaks }}
skulle motsvara:
{{ comment|urlize|linebreaksbr }}
Filter och tidszoner¶
Om du skriver ett anpassat filter som fungerar på datetime
-objekt, registrerar du det vanligtvis med flaggan expects_localtime
inställd på True
:
@register.filter(expects_localtime=True)
def businesshours(value):
try:
return 9 <= value.hour < 17
except AttributeError:
return ""
När denna flagga är inställd, om det första argumentet till ditt filter är en tidszonmedveten datatid, kommer Django att konvertera den till den aktuella tidszonen innan den skickas till ditt filter när det är lämpligt, enligt :ref:regler för tidszonkonverteringar i mallar <time-zones-in-templates>
.
Skriva anpassade malltaggar¶
Taggar är mer komplexa än filter, eftersom taggar kan göra vad som helst. Django tillhandahåller ett antal genvägar som gör det enklare att skriva de flesta typer av taggar. Först ska vi utforska dessa genvägar och sedan förklara hur man skriver en tagg från grunden för de fall då genvägarna inte är tillräckligt kraftfulla.
Enkla taggar¶
- django.template.Library.simple_tag()¶
Många malltaggar tar emot ett antal argument - strängar eller mallvariabler - och returnerar ett resultat efter att ha gjort en viss bearbetning baserad enbart på inmatningsargumenten och viss extern information. Till exempel kan en current_time
-tagg acceptera en formatsträng och returnera tiden som en sträng formaterad i enlighet med detta.
För att underlätta skapandet av dessa typer av taggar tillhandahåller Django en hjälpfunktion, simple_tag
. Denna funktion, som är en metod för django.template.Library
, tar en funktion som accepterar valfritt antal argument, sveper in den i en render
-funktion och de andra nödvändiga bitarna som nämns ovan och registrerar den med mallsystemet.
Vår funktion current_time
kan alltså skrivas så här:
import datetime
from django import template
register = template.Library()
@register.simple_tag
def current_time(format_string):
return datetime.datetime.now().strftime(format_string)
Några saker att notera om hjälpfunktionen simple_tag
:
Kontrollen av det nödvändiga antalet argument etc. har redan gjorts när vår funktion anropas, så vi behöver inte göra det.
Citaten runt argumentet (om sådana finns) har redan tagits bort, så vi får en vanlig sträng.
Om argumentet var en mallvariabel skickas variabelns aktuella värde till vår funktion, inte variabeln i sig.
Till skillnad från andra taggverktyg skickar simple_tag
sin utdata genom conditional_escape()
om mallkontexten är i autoescape-läge, för att säkerställa korrekt HTML och skydda dig från XSS-sårbarheter.
Om ytterligare escaping inte är önskvärt måste du använda mark_safe()
om du är helt säker på att din kod inte innehåller XSS-sårbarheter. För att bygga små HTML-snuttar rekommenderas starkt att använda format_html()
istället för mark_safe()
.
Om din malltagg behöver komma åt den aktuella kontexten kan du använda argumentet takes_context
när du registrerar din tagg:
@register.simple_tag(takes_context=True)
def current_time(context, format_string):
timezone = context["timezone"]
return your_get_current_time_method(timezone, format_string)
Observera att det första argumentet måste heta context
.
För mer information om hur alternativet takes_context
fungerar, se avsnittet om inclusion tags.
Om du behöver byta namn på din tagg kan du ange ett eget namn för den:
register.simple_tag(lambda x: x - 1, name="minusone")
@register.simple_tag(name="minustwo")
def some_function(value):
return value - 2
simple_tag
-funktioner kan acceptera valfritt antal positionella eller nyckelordsargument. Till exempel:
@register.simple_tag
def my_tag(a, b, *args, **kwargs):
warning = kwargs["warning"]
profile = kwargs["profile"]
...
return ...
I mallen kan sedan valfritt antal argument, åtskilda med mellanslag, skickas till malltaggen. Precis som i Python anges värdena för nyckelordsargument med hjälp av likhetstecknet (”=
”) och måste anges efter de positionella argumenten. Till exempel:
{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}
Det är möjligt att lagra taggresultaten i en mallvariabel i stället för att skriva ut dem direkt. Detta görs genom att använda argumentet as
följt av variabelnamnet. På så sätt kan du själv mata ut innehållet där du tycker att det passar:
{% current_time "%Y-%m-%d %I:%M %p" as the_time %}
<p>The time is {{ the_time }}.</p>
Enkla block-taggar¶
- django.template.Library.simple_block_tag()¶
När ett avsnitt av en renderad mall behöver skickas in i en anpassad tagg tillhandahåller Django hjälpfunktionen simple_block_tag
för att åstadkomma detta. I likhet med simple_tag()
accepterar den här funktionen en anpassad taggfunktion, men med det extra argumentet content
, som innehåller det återgivna innehållet som definieras inuti taggen. Detta gör att dynamiska mallavsnitt enkelt kan införlivas i anpassade taggar.
Till exempel kan en anpassad blocktagg som skapar ett diagram se ut så här:
from django import template
from myapp.charts import render_chart
register = template.Library()
@register.simple_block_tag
def chart(content):
return render_chart(source=content)
Argumentet content
innehåller allt mellan taggarna {% chart %}
och {% endchart %}
:
{% chart %}
digraph G {
label = "Chart for {{ request.user }}"
A -> {B C}
}
{% endchart %}
Om det finns andra malltaggar eller variabler i content
-blocket kommer de att renderas innan de skickas till taggfunktionen. I exemplet ovan kommer request.user
att vara löst när render_chart
anropas.
Blocktaggar avslutas med end{name}
(t.ex. endchart
). Detta kan anpassas med parametern end_name
:
@register.simple_block_tag(end_name="endofchart")
def chart(content):
return render_chart(source=content)
Vilket skulle kräva en malldefinition som denna:
{% chart %}
digraph G {
label = "Chart for {{ request.user }}"
A -> {B C}
}
{% endofchart %}
Några saker att notera om simple_block_tag
:
Det första argumentet måste heta
content
, och det kommer att innehålla innehållet i malltaggen som en renderad sträng.Variabler som skickas till taggen inkluderas inte i innehållets renderingskontext, vilket skulle vara fallet om man använde taggen
{% with %}
.
Precis som simple_tag, simple_block_tag
:
Validerar kvantiteten och kvaliteten på argumenten.
Tar bort citat från argumenten om det behövs.
Undviker utgången i enlighet med detta.
Stöder passering av
takes_context=True
vid registreringstillfället för att komma åt kontexten. Observera att i det här fallet måste det första argumentet till den anpassade funktionen mås kallascontext
ochcontent
måste följa efter.Stöder att byta namn på taggen genom att skicka argumentet
name
vid registrering.Accepterar valfritt antal positionella argument eller nyckelordsargument.
Stöder lagring av resultatet i en mallvariabel med hjälp av
as
-varianten.
Innehåll som undkommer
simple_block_tag
beter sig på liknande sätt som simple_tag
när det gäller automatisk escaping. För detaljer om escaping och säkerhet, se simple_tag
. Eftersom argumentet content
redan har återgivits av Django, är det redan escapat.
Ett komplett exempel¶
Tänk dig en anpassad malltagg som genererar en meddelanderuta som stöder flera meddelandenivåer och innehåll utöver en enkel fras. Detta kan implementeras med hjälp av en simple_block_tag
enligt följande:
testapp/templatetags/testapptags.py
¶from django import template
from django.utils.html import format_html
register = template.Library()
@register.simple_block_tag(takes_context=True)
def msgbox(context, content, level):
format_kwargs = {
"level": level.lower(),
"level_title": level.capitalize(),
"content": content,
"open": " open" if level.lower() == "error" else "",
"site": context.get("site", "My Site"),
}
result = """
<div class="msgbox {level}">
<details{open}>
<summary>
<strong>{level_title}</strong>: Please read for <i>{site}</i>
</summary>
<p>
{content}
</p>
</details>
</div>
"""
return format_html(result, **format_kwargs)
I kombination med en minimal vy och motsvarande mall, som visas här:
testapp/views.py
¶from django.shortcuts import render
def simpleblocktag_view(request):
return render(request, "test.html", context={"site": "Important Site"})
testapp/templates/test.html
testapp/templates/test.html
¶{% extends "base.html" %}
{% load testapptags %}
{% block content %}
{% msgbox level="error" %}
Please fix all errors. Further documentation can be found at
<a href="http://example.com">Docs</a>.
{% endmsgbox %}
{% msgbox level="info" %}
More information at: <a href="http://othersite.com">Other Site</a>/
{% endmsgbox %}
{% endblock %}
Följande HTML visas som renderad utdata:
<div class="msgbox error">
<details open>
<summary>
<strong>Error</strong>: Please read for <i>Important Site</i>
</summary>
<p>
Please fix all errors. Further documentation can be found at
<a href="http://example.com">Docs</a>.
</p>
</details>
</div>
<div class="msgbox info">
<details>
<summary>
<strong>Info</strong>: Please read for <i>Important Site</i>
</summary>
<p>
More information at: <a href="http://othersite.com">Other Site</a>
</p>
</details>
</div>
Taggar för inkludering¶
- django.template.Library.inclusion_tag()¶
En annan vanlig typ av malltagg är den typ som visar vissa data genom att rendera en annan mall. Till exempel använder Djangos admin-gränssnitt anpassade malltaggar för att visa knapparna längst ner på formulärsidorna för ”lägg till/ändra”. Dessa knappar ser alltid likadana ut, men länkmålen ändras beroende på vilket objekt som redigeras - så de är ett perfekt fall för att använda en liten mall som är fylld med detaljer från det aktuella objektet. (I administratörens fall är detta taggen submit_row
)
Den här typen av taggar kallas ”inclusion tags”.
Att skriva inclusion-taggar demonstreras förmodligen bäst med ett exempel. Låt oss skriva en tagg som matar ut en lista med val för ett givet Poll
-objekt, som skapades i tutorials. Vi kommer att använda taggen så här:
{% show_results poll %}
…och utdata blir ungefär så här:
<ul>
<li>First choice</li>
<li>Second choice</li>
<li>Third choice</li>
</ul>
Först definierar vi den funktion som tar argumentet och producerar en ordbok med data som resultat. Den viktiga punkten här är att vi bara behöver returnera en ordbok, inte något mer komplext. Detta kommer att användas som en mallkontext för mallfragmentet. Exempel:
def show_results(poll):
choices = poll.choice_set.all()
return {"choices": choices}
Därefter skapar du den mall som används för att rendera taggens utdata. Denna mall är en fast egenskap hos taggen: taggskrivaren specificerar den, inte malldesignern. I vårt exempel är mallen mycket kort:
<ul>
{% for choice in choices %}
<li> {{ choice }} </li>
{% endfor %}
</ul>
Nu skapar och registrerar du inclusion-taggen genom att anropa metoden inclusion_tag()
på ett Library
-objekt. Om vi följer vårt exempel och ovanstående mall finns i en fil som heter results.html
i en katalog som malladdaren söker i, skulle vi registrera taggen så här:
# Here, register is a django.template.Library instance, as before
@register.inclusion_tag("results.html")
def show_results(poll): ...
Alternativt är det möjligt att registrera inkluderingstaggen med hjälp av en django.template.Template
-instans:
from django.template.loader import get_template
t = get_template("results.html")
register.inclusion_tag(t)(show_results)
…när du först skapade funktionen.
Ibland kan dina inkluderingstaggar kräva ett stort antal argument, vilket gör det till en smärta för mallförfattare att skicka in alla argument och komma ihåg deras ordning. För att lösa detta tillhandahåller Django ett takes_context
-alternativ för inkluderingstaggar. Om du anger takes_context
när du skapar en malltagg, kommer taggen inte att ha några nödvändiga argument, och den underliggande Python-funktionen kommer att ha ett argument - mallkontexten från och med när taggen anropades.
Låt oss till exempel säga att du skriver en inclusion-tagg som alltid kommer att användas i ett sammanhang som innehåller variablerna home_link
och home_title
som pekar tillbaka till huvudsidan. Så här skulle Python-funktionen se ut:
@register.inclusion_tag("link.html", takes_context=True)
def jump_link(context):
return {
"link": context["home_link"],
"title": context["home_title"],
}
Observera att den första parametern till funktionen måste heta context
.
I raden register.inclusion_tag()
angav vi takes_context=True
och namnet på mallen. Så här kan mallen link.html
se ut:
Jump directly to <a href="{{ link }}">{{ title }}</a>.
När du sedan vill använda den anpassade taggen laddar du dess bibliotek och anropar den utan några argument, till exempel så här:
{% jump_link %}
Observera att när du använder takes_context=True
behöver du inte skicka några argument till malltaggen. Den får automatiskt tillgång till kontexten.
Parametern takes_context
är som standard inställd på False
. När den är inställd på True
skickas taggen till kontextobjektet, som i det här exemplet. Det är den enda skillnaden mellan det här fallet och det tidigare exemplet med inclusion_tag
.
inclusion_tag
-funktioner kan acceptera valfritt antal positionella argument eller nyckelord. Till exempel:
@register.inclusion_tag("my_template.html")
def my_tag(a, b, *args, **kwargs):
warning = kwargs["warning"]
profile = kwargs["profile"]
...
return ...
I mallen kan sedan valfritt antal argument, åtskilda med mellanslag, skickas till malltaggen. Precis som i Python anges värdena för nyckelordsargument med hjälp av likhetstecknet (”=
”) och måste anges efter de positionella argumenten. Till exempel:
{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}
Avancerade anpassade malltaggar¶
Ibland räcker det inte med de grundläggande funktionerna för att skapa egna malltaggar. Oroa dig inte, Django ger dig fullständig tillgång till de interna funktioner som krävs för att bygga en malltagg från grunden.
En snabb överblick¶
Mallsystemet fungerar i en tvåstegsprocess: kompilering och rendering. När du definierar en anpassad malltagg anger du hur kompileringen och renderingen ska gå till.
När Django kompilerar en mall delar den upp den råa malltexten i ”noder”. Varje nod är en instans av django.template.Node
och har en render()
metod. En kompilerad mall är en lista med Node
-objekt. När du anropar render()
på ett kompilerat mallobjekt, anropar mallen render()
på varje Node
i dess nodlista, med det givna sammanhanget. Alla resultat sammankopplas för att bilda mallens utdata.
För att definiera en anpassad malltagg anger du alltså hur den råa malltaggen konverteras till en Node
(kompileringsfunktionen) och vad nodens render()
-metod gör.
Skriva sammanställningsfunktionen¶
För varje malltagg som mallparsern stöter på anropar den en Python-funktion med taggens innehåll och själva parserobjektet. Denna funktion är ansvarig för att returnera en Node
-instans baserat på innehållet i taggen.
Låt oss till exempel skriva en fullständig implementering av vår malltagg, {% current_time %}
, som visar aktuellt datum/tid, formaterat enligt en parameter som anges i taggen, i strftime()
-syntax. Det är en bra idé att bestämma taggens syntax före allt annat. I vårt fall kan vi säga att taggen ska användas så här:
<p>The time is {% current_time "%Y-%m-%d %I:%M %p" %}.</p>
Parsern för denna funktion ska ta parametern och skapa ett Node
-objekt:
from django import template
def do_current_time(parser, token):
try:
# split_contents() knows not to split quoted strings.
tag_name, format_string = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError(
"%r tag requires a single argument" % token.contents.split()[0]
)
if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
raise template.TemplateSyntaxError(
"%r tag's argument should be in quotes" % tag_name
)
return CurrentTimeNode(format_string[1:-1])
Anteckningar:
parser
är mallens parserobjekt. Vi behöver det inte i det här exemplet.token.contents
är en sträng med taggens råa innehåll. I vårt exempel är det'current_time "%Y-%m-%d %I:%M %p"'
.Metoden
token.split_contents()
separerar argumenten på mellanslag medan citerade strängar hålls ihop. Den mer okompliceradetoken.contents.split()
skulle inte vara lika robust, eftersom den naivt skulle dela på alla mellanslag, inklusive de inom citerade strängar. Det är en bra idé att alltid användatoken.split_contents()
.Denna funktion är ansvarig för att skapa
django.template.TemplateSyntaxError
, med användbara meddelanden, för alla syntaxfel.Undantagen
TemplateSyntaxError
använder variabelntag_name
. Hårdkoda inte taggens namn i dina felmeddelanden, eftersom det kopplar taggens namn till din funktion.token.contents.split()[0]
kommer ’’alltid’’ att vara namnet på din tagg - även när taggen inte har några argument.Funktionen returnerar en
CurrentTimeNode
med allt noden behöver veta om den här taggen. I det här fallet skickar den argumentet –"%Y-%m-%d %I:%M %p"
. De inledande och avslutande citaten från malltaggen tas bort iformat_string[1:-1]
.Parsningen är på mycket låg nivå. Django-utvecklarna har experimenterat med att skriva små ramverk ovanpå detta parsningssystem, med hjälp av tekniker som EBNF-grammatiker, men dessa experiment gjorde mallmotorn för långsam. Den är på låg nivå för att det är snabbast.
Skriva renderingsprogrammet¶
Det andra steget i att skriva egna taggar är att definiera en subklass av Node
som har en render()
-metod.
Om vi fortsätter med exemplet ovan måste vi definiera CurrentTimeNode
:
import datetime
from django import template
class CurrentTimeNode(template.Node):
def __init__(self, format_string):
self.format_string = format_string
def render(self, context):
return datetime.datetime.now().strftime(self.format_string)
Anteckningar:
__init__()
hämtarformat_string
fråndo_current_time()
. Skicka alltid eventuella alternativ/parametrar/argument till enNode
via dess__init__()
.Det är i metoden
render()
som det egentliga arbetet sker.render()
bör i allmänhet misslyckas tyst, särskilt i en produktionsmiljö. I vissa fall dock, särskilt omcontext.template.engine.debug
ärTrue
, kan denna metod ge upphov till ett undantag för att göra felsökningen enklare. Till exempel ger flera kärntaggar upphov tilldjango.template.TemplateSyntaxError
om de får fel antal eller typ av argument.
I slutändan resulterar denna frikoppling av kompilering och återgivning i ett effektivt mallsystem, eftersom en mall kan återge flera kontexter utan att behöva analyseras flera gånger.
Överväganden om automatisk eskapning¶
Utdata från malltaggar körs inte automatiskt genom filtren för automatisk eskapning (med undantag för simple_tag()
som beskrivs ovan). Det finns dock fortfarande ett par saker som du bör tänka på när du skriver en malltagg.
Om render()
-metoden i din malltagg lagrar resultatet i en kontextvariabel (i stället för att returnera resultatet i en sträng), bör den se till att anropa mark_safe()
om så är lämpligt. När variabeln slutligen renderas kommer den att påverkas av den auto-escape-inställning som gäller vid den tidpunkten, så innehåll som bör vara säkert från ytterligare escaping måste markeras som sådant.
Om din malltagg skapar en ny kontext för att utföra en underrendering, ska du också ställa in auto-escape-attributet till den aktuella kontextens värde. Metoden __init__
för klassen Context
tar en parameter som heter autoescape
som du kan använda för detta ändamål. Till exempel:
from django.template import Context
def render(self, context):
# ...
new_context = Context({"var": obj}, autoescape=context.autoescape)
# ... Do something with new_context ...
Detta är inte en särskilt vanlig situation, men det är användbart om du renderar en mall själv. Till exempel:
def render(self, context):
t = context.template.engine.get_template("small_fragment.html")
return t.render(Context({"var": obj}, autoescape=context.autoescape))
Om vi hade försummat att skicka in det aktuella värdet för context.autoescape
till vår nya Context
i det här exemplet skulle resultaten alltid ha escapats automatiskt, vilket kanske inte är det önskade beteendet om malltaggen används i ett {% autoescape off %}
-block.
Säkerhetsöverväganden för trådar¶
När en nod har parsats kan dess render
-metod anropas ett obegränsat antal gånger. Eftersom Django ibland körs i flertrådade miljöer kan en enda nod samtidigt renderas med olika kontexter som svar på två separata förfrågningar. Därför är det viktigt att se till att dina malltaggar är trådsäkra.
För att se till att dina malltaggar är trådsäkra bör du aldrig lagra tillståndsinformation på själva noden. Django tillhandahåller till exempel en inbyggd cycle
malltagg som cyklar mellan en lista med givna strängar varje gång den återges:
{% for o in some_list %}
<tr class="{% cycle 'row1' 'row2' %}">
...
</tr>
{% endfor %}
En naiv implementation av CycleNode
kan se ut ungefär så här:
import itertools
from django import template
class CycleNode(template.Node):
def __init__(self, cyclevars):
self.cycle_iter = itertools.cycle(cyclevars)
def render(self, context):
return next(self.cycle_iter)
Men anta att vi har två mallar som renderar mallutdraget från ovan samtidigt:
Tråd 1 utför sin första loopiteration,
CycleNode.render()
returnerar ’row1’Tråd 2 utför sin första loopiteration,
CycleNode.render()
returnerar ’row2’Tråd 1 utför sin andra loopiteration,
CycleNode.render()
returnerar ’row1’Tråd 2 utför sin andra loopiteration,
CycleNode.render()
returnerar ’row2’
CycleNode itererar, men den itererar globalt. När det gäller tråd 1 och tråd 2 returnerar den alltid samma värde. Detta är inte vad vi vill!
För att lösa detta problem tillhandahåller Django en render_context
som är associerad med contexten
för den mall som för närvarande renderas. render_context
beter sig som en Python-dictionary och bör användas för att lagra Node
-tillstånd mellan anrop av render
-metoden.
Låt oss omarbeta vår CycleNode
-implementering för att använda render_context
:
class CycleNode(template.Node):
def __init__(self, cyclevars):
self.cyclevars = cyclevars
def render(self, context):
if self not in context.render_context:
context.render_context[self] = itertools.cycle(self.cyclevars)
cycle_iter = context.render_context[self]
return next(cycle_iter)
Observera att det är helt säkert att lagra global information som inte kommer att ändras under hela Node
liv som ett attribut. I fallet med CycleNode
, cyclevars
argumentet ändras inte efter Node
är instantiated, så vi behöver inte sätta det i render_context
. Men tillståndsinformation som är specifik för den mall som för närvarande återges, som den aktuella iterationen av CycleNode
, bör lagras i render_context
.
Observera
Observera hur vi använde self
för att omfatta den CycleNode
specifika informationen inom render_context
. Det kan finnas flera CycleNodes
i en given mall, så vi måste vara försiktiga så att vi inte stjäl en annan nods tillståndsinformation. Det enklaste sättet att göra detta är att alltid använda self
som nyckel till render_context
. Om du håller reda på flera tillståndsvariabler kan du göra render_context[self]
till en ordbok.
Registrering av taggen¶
Slutligen, registrera taggen med din moduls Library
instans, som förklaras i :ref:skriva egna malltaggar<howto-writing-custom-template-tags>
ovan. Exempel:
register.tag("current_time", do_current_time)
Metoden tag()
tar två argument:
Namnet på malltaggen - en sträng. Om detta utelämnas kommer namnet på kompileringsfunktionen att användas.
Kompileringsfunktionen – en Python-funktion (inte namnet på funktionen som en sträng).
Precis som med filterregistrering är det också möjligt att använda detta som en dekorator:
@register.tag(name="current_time")
def do_current_time(parser, token): ...
@register.tag
def shout(parser, token): ...
Om du utelämnar argumentet name
, som i det andra exemplet ovan, kommer Django att använda funktionens namn som taggnamn.
Överföring av mallvariabler till taggen¶
Även om du kan skicka valfritt antal argument till en malltagg med hjälp av token.split_contents()
, packas alla argument upp som stränglitteraler. Det krävs lite mer arbete för att skicka dynamiskt innehåll (en mallvariabel) till en malltagg som ett argument.
Medan de tidigare exemplen har formaterat den aktuella tiden till en sträng och returnerat strängen, anta att du vill skicka in en DateTimeField
från ett objekt och låta malltaggen formatera datumtiden:
<p>This post was last updated at {% format_time blog_entry.date_updated "%Y-%m-%d %I:%M %p" %}.</p>
Initialt kommer token.split_contents()
att returnera tre värden:
Taggens namn
format_time
.Strängen
'blog_entry.date_updated'
(utan omgivande citattecken).Formateringssträngen
'"%Y-%m-%d %I:%M %p"'
. Returvärdet frånsplit_contents()
kommer att inkludera inledande och avslutande citattecken för stränglitteraler som denna.
Nu bör din tagg börja se ut så här:
from django import template
def do_format_time(parser, token):
try:
# split_contents() knows not to split quoted strings.
tag_name, date_to_be_formatted, format_string = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError(
"%r tag requires exactly two arguments" % token.contents.split()[0]
)
if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
raise template.TemplateSyntaxError(
"%r tag's argument should be in quotes" % tag_name
)
return FormatTimeNode(date_to_be_formatted, format_string[1:-1])
Du måste också ändra renderaren så att den hämtar det faktiska innehållet i egenskapen date_updated
i objektet blog_entry
. Detta kan åstadkommas genom att använda klassen Variable()
i django.template
.
För att använda klassen Variable
, instansiera den med namnet på den variabel som ska lösas och anropa sedan variable.resolve(context)
. Så här gör du till exempel:
class FormatTimeNode(template.Node):
def __init__(self, date_to_be_formatted, format_string):
self.date_to_be_formatted = template.Variable(date_to_be_formatted)
self.format_string = format_string
def render(self, context):
try:
actual_date = self.date_to_be_formatted.resolve(context)
return actual_date.strftime(self.format_string)
except template.VariableDoesNotExist:
return ""
Variabelupplösning ger upphov till undantaget VariableDoesNotExist
om den inte kan lösa upp den sträng som skickas till den i sidans aktuella kontext.
Ställa in en variabel i kontexten¶
Exemplen ovan matar ut ett värde. I allmänhet är det mer flexibelt om dina malltaggar ställer in mallvariabler istället för att mata ut värden. På så sätt kan mallförfattare återanvända de värden som dina malltaggar skapar.
Om du vill ställa in en variabel i kontexten använder du dictionary assignment på kontextobjektet i metoden render()
. Här är en uppdaterad version av CurrentTimeNode
som ställer in en mallvariabel current_time
istället för att skriva ut den:
import datetime
from django import template
class CurrentTimeNode2(template.Node):
def __init__(self, format_string):
self.format_string = format_string
def render(self, context):
context["current_time"] = datetime.datetime.now().strftime(self.format_string)
return ""
Observera att render()
returnerar en tom sträng. render()
bör alltid returnera strängar. Om allt malltaggen gör är att ställa in en variabel, ska render()
returnera den tomma strängen.
Så här använder du den här nya versionen av taggen:
{% current_time "%Y-%m-%d %I:%M %p" %}<p>The time is {{ current_time }}.</p>
Variabelns omfattning i sitt sammanhang
Alla variabler som anges i kontexten kommer endast att vara tillgängliga i samma block
i mallen där de tilldelades. Det här beteendet är avsiktligt; det ger ett utrymme för variabler så att de inte står i konflikt med kontexten i andra block.
Men det finns ett problem med CurrentTimeNode2
: Variabelnamnet current_time
är hårdkodat. Detta innebär att du måste se till att din mall inte använder {{ current_time }}
någon annanstans, eftersom {% current_time %}
blint kommer att skriva över variabelns värde. En renare lösning är att låta malltaggen ange namnet på utdatavariabeln, så här:
{% current_time "%Y-%m-%d %I:%M %p" as my_current_time %}
<p>The current time is {{ my_current_time }}.</p>
För att göra det måste du refaktorisera både kompileringsfunktionen och Node
-klassen, så här:
import re
class CurrentTimeNode3(template.Node):
def __init__(self, format_string, var_name):
self.format_string = format_string
self.var_name = var_name
def render(self, context):
context[self.var_name] = datetime.datetime.now().strftime(self.format_string)
return ""
def do_current_time(parser, token):
# This version uses a regular expression to parse tag contents.
try:
# Splitting by None == splitting by spaces.
tag_name, arg = token.contents.split(None, 1)
except ValueError:
raise template.TemplateSyntaxError(
"%r tag requires arguments" % token.contents.split()[0]
)
m = re.search(r"(.*?) as (\w+)", arg)
if not m:
raise template.TemplateSyntaxError("%r tag had invalid arguments" % tag_name)
format_string, var_name = m.groups()
if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
raise template.TemplateSyntaxError(
"%r tag's argument should be in quotes" % tag_name
)
return CurrentTimeNode3(format_string[1:-1], var_name)
Skillnaden här är att do_current_time()
hämtar formatsträngen och variabelnamnet och skickar båda till CurrentTimeNode3
.
Slutligen, om du bara behöver ha en enkel syntax för din anpassade kontextuppdaterande malltagg, kan du överväga att använda simple_tag()
-genvägen, som stöder tilldelning av taggresultaten till en mallvariabel.
Parsning tills en annan blocktagg¶
Malltaggar kan fungera tillsammans. Till exempel döljer standardtaggen {% comment %}
allt fram till {% endcomment %}
. För att skapa en malltagg som den här använder du parser.parse()
i din kompileringsfunktion.
Så här kan en förenklad {% comment %}
tagg implementeras:
def do_comment(parser, token):
nodelist = parser.parse(("endcomment",))
parser.delete_first_token()
return CommentNode()
class CommentNode(template.Node):
def render(self, context):
return ""
Observera
Den faktiska implementeringen av {% comment %}
är något annorlunda i det att den tillåter brutna malltaggar att visas mellan {% comment %}
och {% endcomment %}
. Detta görs genom att anropa parser.skip_past('endcomment')
istället för parser.parse(('endcomment',)))
följt av parser.delete_first_token()
, vilket gör att en nodlista inte genereras.
parser.parse()
tar en tupel av namn på blocktaggar ’’att analysera fram till’’. Den returnerar en instans av django.template.NodeList
, som är en lista över alla Node
-objekt som parsern stötte på ’’innan’’ den stötte på någon av de taggar som anges i tupeln.
I "nodelist = parser.parse(('endcomment',))"
i exemplet ovan är nodelist
en lista över alla noder mellan {% comment %}
och {% endcomment %}
, exklusive {% comment %}
och {% endcomment %}
själva.
När parser.parse()
har anropats har parsern ännu inte ”konsumerat” taggen {% endcomment %}
, så koden måste uttryckligen anropa parser.delete_first_token()
.
CommentNode.render()
returnerar en tom sträng. Allt mellan {% comment %}
och {% endcomment %}
ignoreras.
Parsning till en annan blocktagg och lagring av innehåll¶
I det föregående exemplet kastade do_comment()
bort allt mellan {% comment %}
och {% endcomment %}
. Istället för att göra det är det möjligt att göra något med koden mellan blocktaggar.
Här är till exempel en anpassad malltagg, {% upper %}
, som skriver allt mellan sig själv och {% endupper %}
med stor bokstav.
Användning:
{% upper %}This will appear in uppercase, {{ your_name }}.{% endupper %}
Precis som i det föregående exemplet använder vi parser.parse()
. Men den här gången skickar vi den resulterande nodelist
till Node
:
def do_upper(parser, token):
nodelist = parser.parse(("endupper",))
parser.delete_first_token()
return UpperNode(nodelist)
class UpperNode(template.Node):
def __init__(self, nodelist):
self.nodelist = nodelist
def render(self, context):
output = self.nodelist.render(context)
return output.upper()
Det enda nya konceptet här är self.nodelist.render(context)
i UpperNode.render()
.
För fler exempel på komplex rendering, se källkoden för {% for %}
i django/template/defaulttags.py och {% if %}
i django/template/smartif.py.