Come creare tag di template e filtri personalizzati

Il linguaggio di template di Django arriva come un’ampia varietà di filtri e tag built-in progettati per far fronte alle necessità di logica di presentazione delle tue applicazioni. Eppure, potresti trovarti nella condizione di aver bisogno di funzionalità che non sono coperte dal set di primitive di template del core. Puoi estendere il motore di template definendo dei tag e filtri personalizzati usando Python e poi renderli disponibili ai tuoi template utilizzando il tag {% load %}

Layout del codice

Il posto più comune in cui specificare tag e filtri di template personalizzati è nell’applicazione Django. Se sono collegati ad una app esistente, ha senso metterli lì; altrimento, possono essere aggiunti ad una nuova app. Quando una applicazione Django viene aggiunta ad INSTALLED_APPS, ogni tag che definisce nella locazione convenzionale descritta qui sotto viene automaticamente reso disponibile al caricamento nei template.

L’applicazione deve contenere una directory templatetags, allo stesso livello di models.py, views.py, ecc. Se non esiste già, creala - non dimenticare il file __init__.py per assicurarti che la directory sia trattata come un package Python.

I server di sviluppo non si riavviano in automatico

Dopo aver aggiunto il modulo templatetags, avrai bisogno di riavviare il tuo server prima di poter utilizzare i tag o i filtri nei template.

I tuoi tag e filtri personalizzati vivranno in un modulo nella directory templatetags. Il nome del file del modulo è il nome che userai per caricare successivamente i tag, quindi fai attenzione a sceglierne uno che non vada in conflitto con i tag ed i filtri personalizzati di un’altra applicazione.

Per esempio, se i tags/filtri personalizzati sono in un file chiamato poll_extras.py, il layout della tua applicazione potrebbe assomigliare a questo:

polls/
    __init__.py
    models.py
    templatetags/
        __init__.py
        poll_extras.py
    views.py

E nel tuo template utilizzerai i seguenti:

{% load poll_extras %}

Le app che contengono i tag personalizzati devono trovarsi in INSTALLED_APPS affinchè il tag {% load %} funzioni. E” una feature di sicurezza: ti rende possibile fare hosting di codice Python per molte librerie di template su una singola macchina host senza abilitare l’accesso a tutte per ogni installazione di Django.

Non c’è limite al numero di moduli che puoi aggiungere nel package templatetags. Tieni solo presente che uno statement {% load %} caricherà tag/filtri per un dato nome di modulo Python, non per il nome dell’app.

Per essere una libreria di tag valida, il modulo deve contenere una variabile module-level chiamata register, che è una instanza di template.Library, nella quale sono registrati tutti i tag ed i filtri. Così, nella parte più alta del tuo modulo, metti quanto segue:

from django import template

register = template.Library()

Alternativamente, i moduli per i tag di template possono essere registrati attraverso l’argomento 'libraries' in DjangoTemplates. Risulta utile quando vuoi usare una label differente rispetto al nome del modulo di tag di template quando carichi i tag di template. Ti permette anche di registrare tag senza installare una applicazione.

Dietro le quinte

Per molti più esempi, leggi il codice sorgente dei filtri di default e dei tag di Django. Si trovano rispettivamente in django/template/defaultfilters.py e django/template/defaulttags.py.

Per avere più informazioni sul tag load, leggi la sua documentazione.

Scrivere filtri template personalizzati

I filtri custom sono funzioni Python che prendono uno o due argomenti.

  • Il valore della variabile (input) - non necessariamente una stringa.
  • Il valore dell’argomento - può avere un valore di default o può essere lasciata fuori.

Per esempio, nel filtro «{{ var|foo:»bar» }}», al filtro foo sarà passata la variabile «var» e l’argomento «bar».

Dal momento che il linguaggio di template non fornisce la gestione delle eccezioni, ogni eccezione sollevata dal un filtro di template sarà esposta come errore del server. Quindi, le funzioni di filtro dovrebbero evitare di sollevare eccezioni se c’è un valore di fallback ragionevole da restituire. In caso di input che rappresenti un chiaro bug in un template, sollevare una eccezione può ancora essere meglio di un fallimento silente che nasconde il bug.

Un esempio della definizione di un filtro

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

E qui un esempio di come il viene usato

{{ somevariable|cut:"0" }}

Moltri filtri non prendono argomenti. In questo caso, lascia l’argomento fuori dalla tua funzione:

def lower(value): # Only one argument.
    """Converts a string into all lowercase"""
    return value.lower()

Registrazione di filtri personalizzati

django.template.Library.filter()

Dopo aver scritto la definizione del filtro, bisogna registrarlo con l’istanza di «Library» al fine di renderlo disponibile per l’uso nel linguaggio dei template di Django.

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

Il metodo «Library.filter()» prende due argomenti:

  1. Il nome del filtro – una stringa
  2. La funzione di compilazione –una funzione Python (non il nome di funzione come stringa).

In alternativa puoi usare register.filter() come decorator

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

@register.filter
def lower(value):
    return value.lower()

Se lasci fuori l’argomento name, come nell’esempio sopra, Django userà il nome della funzione come nome di filtro.

Infine, register.filter() accetta anche tre argomenti keyword, is_safe, needs_autoescape e expects_localtime. Questi argomenti sono descritti in filtri ed auto-escaping e filtri e time zones qui sotto.

Template filtri che si aspettano delle stringhe

django.template.defaultfilters.stringfilter()

Se stai scrivendo un filtro di template che si aspetta solo una stringa come primo argomento, dovresti utilizzare il decoratore stringfilter. Questo convertirà un oggetto al suo valore stringa prima di passarlo alla tua funzione:

from django import template
from django.template.defaultfilters import stringfilter

register = template.Library()

@register.filter
@stringfilter
def lower(value):
    return value.lower()

In questo modo, sarà in grado di passare, diciamo, un intero a questo filtro e non causerà un AttributeError (perchè gli integer non hanno metodi lower())

Filtri e auto escaping

Scrivendo un filtro personalizzato, pensa a come il filtro interagirà con i comportamenti di auto-escaping di Django. Nota che nel codice di template possono essere passati due tipi di stringhe:

  • Le Raw strings sono le stringhe native di Python. All’output, sono oggetto di escape se è attivo l’auto-escape o altrimenti presentate senza cambiamenti.

  • Le Safe strings sono stringhe che sono state marcate come safe da ulteriori escape a tempo di output. Ogni escaping necessario è già stato fatto. Sono usate comunemente per output che contiene HTML grezzo che deve essere interpretato così come è client-side.

    Internamente, queste stringhe sono di tipo SafeString.  Puoi testarle utilizzando del codice come questo:

    from django.utils.safestring import SafeString
    
    if isinstance(value, SafeString):
        # Do something with the "safe" string.
        ...
    

Il codice dei filtri di template ricade in una delle due situazioni:

  1. Il tuo filtro non introduce caratteri HTML non sicuri (<, >, ', " or &) nei risultati che non erano già presenti. In questo caso, puoi lasciare che Django si curi di fare auto-escape per te. Tutto quel che devi fare è impostare is_safe a True quando registri la funzione di filtro, così:

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

    Questo flag dice a Django che se una striga «safe» viene passata nel filtro, il risultato sarà ancora «safe» e se viene passata una stringa «non safe», Django farà automaticamente escape, se necessario.

    Puoi pensarlo come «questo filtro è sicuro – non introduce alcuna possibilità di HTML non safe».

    La ragione per la quale is_safe è necessario è perchè ci sono molte operazioni sulle stringhe che trasformano un oggetto SafeData in uno oggetto str normale e, piuttosto che cercare di catturarle tutte, cosa che sarebbe molto difficile, Django ripara il danno dopo aver completato il filtro.

    Per esempio, supponi di avere un filtro che aggiunge xx alla fine di ogni input. Siccome non introduce caratteri HTML pericolosi al risultato (tranne che nel caso non ci siano già), dovresti marcare il tuo filtro con is_safe:

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

    Quando questo filtro viene utilizzato in un template quando l’auto-escape è abilitato, Django farà escape dell’output in tutti i casi in cui l’input non sia marcato come «safe».

    Di default, is_safe è False e puoi ometterlo da tutti i filtri in cui non sia required.

    Fai attenzione quando decidi se il tuo filtro davvero lascia le stringhe safe come safe. Se stai rimuovendo caratteri, potresti lasciare inavvertitamente tag HTML non bilanciati o entità nei risultati. Per esempio, rimuovere un > dall’input potrebbe convertire 1 in <a, che avrebbe bisogno di escaping sull’output per evitare problemi. In modo simile, rimuovere un punto e virgola (;) può trasformare &amp; in &amp, che non è più una entità valida e quindi necessità di ulteriore escaping. In molti casi non sarà così complicato ma fai attenzione a problemi come questi quando rivedi il tuo codice.

    Marcare un filtro come is_safe forzerà il valore restituito a stringa. Se il tuo filtro dovrebbe restituire un booleano o altri valori non stringa, marcarlo come is_safe probabilmente avrà conseguenze non volute (come la conversione del booleano False nella stringa “False”).

  2. Alternativamente, il tuo codice può occuparsi manualmente di ogni escaping necessario. Questo è necessario quando introduci del nuovo markup HTML nei risultati. Potresti voler marcare l’output come safe così da non introdurre ulteriore escaping ed in questo caso dovrai occuparti da te dell’input.

    Per marcare l’output come stringa safe, usa django.utils.safestring.mark_safe().

    Stai attento però. Devi fare altro oltre a marcare l’output come safe. Devi assicurarti che sia davvero safe e quel che fai dipende dal fatto che l’auto-escaping sia attivo. L’idea è scrivere filtri che possano operare in template in cui l’auto-escape sia attivo o meno per rendere più facili le cose agli autori dei template.

    Perchè il filtro conosca lo stato corrente di auto-escape, imposta il flag needs_autoescape a True quando registri la tua funzione di filtro (se non lo specifichi, di default è impostato a False). Questo flag dice a Django che il tuo filtro vuole che gli sia passato un argomento extra chiamato autoescape, che è True se l’auto-escape è attivo e False altrimenti. Si raccomanda di impostare il default sull’auto-escape a True, così che chiamando la funzione dal codice Python l’escape sarà abilitato di default.

    Ad esempio, scriviamo un filtro che enfatizza il primo carattere di una stringa:

    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)
    

    Il flag needs_autoescape e l’argomento keyword autoescape fanno sì che la funzione sappia quando l’escaping automatico è attivo nel momento in cui il filtro viene chiamato. Usiamo autoescape per decidere quando l’input debba essere passato in django.utils.html.conditional_escape o meno. (In quest’ultimo caso, usiamo la funzione identità come funzione di «escape»). La funzione conditional_escape() è come escape() a parte per il fatto che fa escaping dell’input che non è una instanza di SafeData. Se una istanza di SafeData  viene passata a conditional_escape(), i dati vengono restituiti così come sono.

    Infine, nell’esempio sopra, ci ricordiamo di marcare il risultato come safe in modo che il nostro HTML venga inserito direttamente nel template senza ulteriore escaping.

    Non c’è bisogno di preoccuparsi del flag is_safe in questo caso (anche se includerlo non farebbe male). Ogni volta che gestisci manualmente i problemi di auto-escape e restituisci una stringa safe, il flag is_safe non cambierebbe comunque niente.

Avvertimento

Evitare le vulnerabilità XSS quando si riutilizzano i filtri built-in

I filtri built-in di Django hanno di default «autoescape=True» al fine di assicurare il corretto autoescaping ed evitare vulnerabilità di tipo cross-site script.

Nelle vecchie versioni di Django, fai attenzione quando riusi i filtri built-in di Django perchè autoescape viene impostato di default a None. Dovrai passare autoescape=True per fare autoescaping.

Per esempio, se volessi scrivere un filtro custom chiamato urlize_and_linebreaks che combina i filtri urlize e linebreaksbr, avrebbe questo aspetto:

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
    )

Poi

{{ comment|urlize_and_linebreaks }}

sarebbe equivalente a:

{{ comment|urlize|linebreaksbr }}

Filtri e time zones

Se scrivessi un filtro custom che opera sugli oggetti datetime, normalmente lo registreresti con il flag expects_localtime a True:

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

Quando questo flag è impostato, se il primo argomento del tuo filtro è un datetime con time zone, Django lo converte alla time zone corrente prima di passarlo al tuo filtro ove appropriato, secondo le regole per le conversioni delle time zone nei template.

Scrivere template tag personalizzati

I tag sono più complessi dei filtri, perchè i tag possono fare qualsiasi cosa. Django offre degli shortcut che rendono la scrittura di molti tipi di tag più semplice. Prima esploreremo questi shortcut, poi spiegheremo come scrivere un tag da zero per quei casi in cui gli shortcut non bastano.

Semplici tag

django.template.Library.simple_tag()

Molti template tag prendono un certo numero di argomenti – stringhe o variabili di template – e restituiscono un risultato dopo aver fatto qualche processamento basato esclusivamente sugli argomenti di input e qualche informazione esterna. Per esempio, un tag current_time potrebbe accettare una stringa di formattazione e restituire l’ora come stringa formatta di conseguenza.

Per facilitare la creazione di questi tipi di tag, Django fornisce una funzione helper, simple_tag. Questa funzione, che è un metodo di django.template.Library, prende una funzione che accetta un numero arbitrario di argomenti, la racchiude in una funzione render e le altre parti necessarie menzionate prima e la registra nel sistema di template.

La nostra funzione current_time dovrebbe essere scritta così:

import datetime
from django import template

register = template.Library()

@register.simple_tag
def current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

Alcune cose da notare a proposito della funzione helper simple_tag:

  • Controllare il numero richiesto di argomenti ecc…viene già fatto quando la funzione viene chiamata, quindi non dobbiamo farlo.
  • Le virgolette attorno all’argomento (se ce ne sono) sono già state eliminate, quindi riceviamo una stringa semplice.
  • Se l’argomento era una variabile di template, alla nostra funzione viene passato il valore corrente della variabile, non la variabile stessa.

A differenza di altre utilità di tag, simple_tag passa il suo output attraverso conditional_escape() se il template context è in modalità autoescape, per assicurare un HTML corretto e proteggerti da vulnerabilità XSS.

Se non si desidera un escaping aggiuntivo, sarà necessario utilizzare mark_safe() solo se si è assolutamente sicuri che il codice non contenga vulnerabilità XSS. Per creare piccoli frammenti di codice HTML, è fortemente raccomandato l’uso di format_html() invece di mark_safe().

Se il tuo tag nel template ha bisogno di accedere al context corrente, puoi usare l’argomento takes_context quando registri il tuo tag:

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

Nota che il primo argomento deve chiamarsi «context»

Per ulteriori informazioni su come funziona l’opzione takes_context vedere la sezione su inclusion tags.

Se hai bisogno dirinominare il tag, puoi fornirne un nome personalizzato:

register.simple_tag(lambda x: x - 1, name='minusone')

@register.simple_tag(name='minustwo')
def some_function(value):
    return value - 2

Le funzioni simple_tag possono accettare un numero qualsiasi di argomenti posizionali o di parole chiave. Per esempio:

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

Quindi nel template qualsiasi argomento, separato da spazi, può essere passato al tag del template. Come in Python, i valori per gli argomenti delle parole chiave sono impostati usando il segno di uguale (»=») e devono essere inseriti dopo gli argomenti posizionali. Per esempio:

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

È possibile memorizzare i risultati del tag in una variabile del template anziché esporlo direttamente. Questo viene fatto usando l’argomento as seguito dal nome della variabile. In questo modo puoi mostrare il contenuto dove ritieni opportuno:

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

Tag di inclusione

django.template.Library.inclusion_tag()

Un altro tipo comune di tag template è il tipo che visualizza alcuni dati eseguendo il rendering di un altro template. Ad esempio, l’interfaccia di amministrazione di Django utilizza tag template personalizzati per visualizzare i pulsanti «aggiungi/modifica» nella parte inferiore delle pagine modulo (form). Quei pulsanti hanno sempre lo stesso aspetto, ma le destinazioni del collegamento cambiano a seconda dell’oggetto che viene modificato, quindi sono un caso perfetto per l’utilizzo di un piccolo template pieno di dettagli dall’oggetto corrente. (Nel caso dell’amministratore, questo è il tag submit_row.)

I tag di questo tipo sono detti «tag di inclusione»

La scrittura di tag di inclusione è probabilmente meglio dimostrata dall’esempio. Scriviamo un tag che produca un elenco di scelte per un dato oggetto Poll, come è stato creato in tutorials. Useremo il tag in questo modo:

{% show_results poll %}

… e l’output sarà qualcosa di simile a questo:

<ul>
  <li>First choice</li>
  <li>Second choice</li>
  <li>Third choice</li>
</ul>

Innanzitutto, definisci la funzione che accetta l’argomento e produce un dizionario di dati per il risultato. Il punto importante qui è che dobbiamo solo restituire un dizionario, non qualcosa di più complesso. Questo verrà utilizzato come context del template per il frammento del template. Esempio:

def show_results(poll):
    choices = poll.choice_set.all()
    return {'choices': choices}

Quindi, crea il template utilizzato per eseguire il rendering dell’output dei tag. Questo modello è una caratteristica fissa dei tag: lo specifica l’autore del tag, non il designer del template. Seguendo il nostro esempio, il template è molto breve:

<ul>
{% for choice in choices %}
    <li> {{ choice }} </li>
{% endfor %}
</ul>

Ora, crea e registra il tag di inclusione chiamando il metodo inclusion_tag() su un oggetto Library. Seguendo il nostro esempio, se il template sopra si trova in un file chiamato results.html in una directory che viene cercata dall’invocatore del template, registreremo il tag in questo modo:

# Here, register is a django.template.Library instance, as before
@register.inclusion_tag('results.html')
def show_results(poll):
    ...

In alternativa è possibile registrare il tag di inclusione utilizzando un’istanza di django.template.Template:

from django.template.loader import get_template
t = get_template('results.html')
register.inclusion_tag(t)(show_results)

…alla prima creazione della funzione.

A volte, i tag di inclusione potrebbero richiedere un numero elevato di argomenti, rendendo difficile per gli autori dei template passare tutti gli argomenti e ricordare il loro ordine. Per risolvere questo problema, Django fornisce un’opzione takes_context per i tag di inclusione. Se specifichi takes_context nella creazione di un tag nel template, il tag non avrà argomenti richiesti e la funzione Python sottostante avrà un argomento – il context del template a partire da quando il tag è stato chiamato.

Ad esempio, supponiamo che tu stia scrivendo un tag di inclusione che sarà sempre utilizzato in un context che contiene variabili home_link e home_title che puntano alla pagina principale. Ecco come sarebbe la funzione Python:

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

Si noti che il primo parametro della funzione deve essere chiamato context.

In quella riga register.inclusion_tag(), abbiamo specificato takes_context=True e il nome del template. Ecco come potrebbe apparire il template link.html:

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

Quindi, ogni volta che desideri utilizzare quel tag personalizzato, carica la sua libreria e chiamala senza argomenti, in questo modo:

{% jump_link %}

Nota che quando usi takes_context=True, non è necessario passare argomenti al tag del modello. Ottiene automaticamente l’accesso al context.

Il parametro takes_context è predefinito su False. Quando è impostato su True, al tag viene passato l’oggetto context, come in questo esempio. Questa è l’unica differenza tra questo caso e il precedente esempio di inclusion_tag.

Le funzioni inclusion_tag possono accettare qualsiasi argomento posizionale o di parole chiave. Per esempio:

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

Quindi nel template qualsiasi argomento, separato da spazi, può essere passato al tag del template. Come in Python, i valori per gli argomenti delle parole chiave sono impostati usando il segno di uguale (»=») e devono essere inseriti dopo gli argomenti posizionali. Per esempio:

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

Tag di template personalizzati avanzati

A volte le funzionalità di base per la creazione di tag in template personalizzati non sono sufficienti. Non preoccuparti, Django ti dà accesso completo al necessario per creare un tag template da zero.

Una rapida panoramica

Il sistema di template funziona in un processo in due fasi: compilazione e rendering. Per definire un tag template personalizzato, specifica come funziona la compilation e come funziona il rendering.

Quando Django compila un template, divide il testo del template grezzo in “”nodi””. Ogni nodo è un’istanza di django.template.Node e ha un metodo render(). Un template compilato è un elenco di oggetti Nodo. Quando chiami render() su un oggetto del template compilato, il template chiama render() su ogni Nodo nella sua lista dei nodi, con il context fornito. I risultati sono tutti concatenati insieme per formare il risultato (output) del template.

Quindi, per definire un custom template tag: specifica come il template tag grezzo viene convertito in un Node (usa la funzione di compilazione) e definisci cosa fa il metodo render() del nodo.

Scrivere la funzione di compilazione

Per ogni tag del template che incontra il parser del template, chiama una funzione Python con il contenuto del tag e l’oggetto del parser stesso. Questa funzione è responsabile della restituzione di un’istanza Node basata sul contenuto del template tag.

Per esempio, scriviamo un’implementazione completa del nostro tag template, {% current_time %}, che mostra la data/ora corrente, formattata secondo un parametro dato nel tag, sintassi :func:`~time.strftime ` . È una buona idea decidere la sintassi dei template tag prima di ogni altra cosa. Nel nostro caso, supponiamo che il tag debba essere utilizzato in questo modo:

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

Il parser per questa funzione dovrebbe prendere il parametro e creare un oggetto Node:

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])

Note:

  • parser è l’oggetto parser di template. Non ne abbiamo bisogno in questo esempio.
  • token.contents è una stringa di contenuti grezzi del tag. Nel nostro esempio, è 'current_time "%Y-%m-%d %I:%M %p"'.
  • Il metodo token.split_contents() separa gli argomenti con gli spazi tenendo assieme le stringhe tra virgolette. Il più diretto token.contents.split() non sarebbe così robusto, perchè dividerebbe pedissequamente tutti gli spazi, inclusi quelli tra virgolette. E” una buona idea usare sempre token.split_contents()`.
  • Questa funzione è responsabile di sollevare django.template.TemplateSyntaxError, con messaggi utili, per ogni errore di sintassi.
  • Le eccezioni TemplateSyntaxError usano la variabile tag_name. Non fare hard-coding del nome del tag nei tuoi messaggi di errore, perchè questo accoppia il nome del tuo tag alla funzione. token.contents.split()[0] sarò «sempre» il nome del tuo tag – anche quando il tag non prende argomenti.
  • La funzione restituisce un CurrentTimeNode con tutto ciò di cui il nodo ha bisogno di sapere circa questo tag. In questo caso, passa l’argomento –"%Y-%m-%d %I:%M %p". Le virgolette in testa ed in coda dal tag di template sono rimosse in format_string[1:-1].
  • Il parsing è molto di basso livello. Gli sviluppatori di Django hanno sperimentato nello scrivere piccoli framework su questo sistema di parsing, usando tecniche con le grammatiche EBNF, ma questi esperimenti hanno reso il motore di template troppo lento. E” di basso livello perchè in questo modo è più veloce.

Scrivere il renderer

Il secondo passo nello scrivere tag personalizzati è definire una sottoclasse Node che ha un metodo render().

Continuando l’esempio precedente, dobbiamo definire 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)

Note:

  • __init__() prende format_string da do_current_time(). Passa sempre qualsiasi opzione/parametro/argomento ad un Node tramite il suo __init__().
  • Il metodo render() è dove viene fatto il lavoro vero e proprio.
  • render() generalmente dovrebbe fallire in modo silenzioso, in particolare in un ambiente di produzione. In qualche caso tuttavia, specialmente se context.template.engine.debug è impostato a True, questo metodo può sollevare una eccezione per facilitare il debugging. Per esempio, molti tag core sollevano django.template.TemplateSyntaxError se ricevono un numero di argomenti sbagliato.

In ultimo, questo disaccoppiamento tra compilazione e rendering ha come risultato un sistema di template efficiente, perchè un template può fare render di diversi contesti senza dover essere analizzato più volte.

Considerazioni sull’auto-escape

L’output da template tag non è automaticamente passato ai filtri di auto-escape (con l’eccezione di simple_tag() come descritto sopra). Comunque, ci sono ancora un paio di cose che dovresti tenere a mente quando scrivi un tag di template.

Se il metodo render() del tuo template tag immagazzina il risultato in una variabile di contesto (piuttosto che restituirlo in una stringa), dovrebbe fare attenzione a chiamare mark_safe() quando è opportuno. Quando la variabile viene resa, alla fine, in quel momento sarà influenzata dalle impostazioni di auto-escape, così il contenuto che dovrebbe essere sicuro rispetto ad un ulteriore escaping dovrebbe essere marcato come tale.

Inoltre, se il tuo tag di template crea un nuovo contesto per fare qualche sotto-rendering, imposta l’attributo di auto-escape al valore corrente di contesto. Il metodo __init__ per la classe Context prende un parametro chiamato autoescape che puoi usare per questo scopo. Per esempio:

from django.template import Context

def render(self, context):
    # ...
    new_context = Context({'var': obj}, autoescape=context.autoescape)
    # ... Do something with new_context ...

Non è una situazione molto comune, ma è utile se si fa rendering di un template da soli. Per esempio:

def render(self, context):
    t = context.template.engine.get_template('small_fragment.html')
    return t.render(Context({'var': obj}, autoescape=context.autoescape))

Se non ci fossimo curati di passare il valore context.autoescape al nostro nuovo Context in questo esempio, sui risultati sarebbe sempre stato fatto automaticamente escape, il che avrebbe potuto non essere il comportamento desiderato se il tag di template fosse stato usato in un blocco {% autoescape off %}

Considerazioni sulla sicurezza

Una volta che un nodo viene analizzato, il suo metodo render può essere chiamato un numero indefinito di volte. Dal momento che Django ogni tanto gira in ambienti multi-threaded, un singolo nodo può essere simultaneamente reso con differenti contesti in risposta a richieste differenti. Quindi, è importante assicurarsi che i propri template tag siano thread safe.

Per essere sicuro che i template tag siano thread safe, non dovresti mai immagazzinare informazioni di stato sul nodo stesso. Per esempio, Django offre un template tag builtin cycle che compie un ciclo lungo una lista di date stringhe ogni volta che viene visualizzato:

{% for o in some_list %}
    <tr class="{% cycle 'row1' 'row2' %}">
        ...
    </tr>
{% endfor %}

Una implementazione naive di CycleNode può avere un aspetto simile:

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)

Ma, supponiamo di avere due template che visualizzano lo snippet di template qui sopra allo stesso tempo:

  1. Il thread 1 esegue la sua prima iterazione di loop, CycleNode.render() restituisce “row1”
  2. Il thread 2 esegue la sua prima iterazione di loop, CycleNode.render() restituisce “row2”
  3. Il thread 1 esegue la sua seconda iterazione di loop, CycleNode.render() restituisce “row1”
  4. Il thread 2 esegue la sua seconda iterazione di loop, CycleNode.render() restituisce “row2”

CycleNode sta iterando, ma sta iterando globalmente. Per quel che ne sanno Thread 1 e Thread 2, sta restituendo sempre lo stesso valore. Non è quello che vogliamo!

Per risolvere questo problema, Django offre un render_context che è associato con il context del template che si mostra in quel momento. render_context si comporta come un dizionario Python e dovrebbe essere usato per memorizzare lo stato di Node tra le invocazioni del metodo render.

Rifattorizziamo la nostra implementazione di CycleNode per usare 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)

Nota che è perfettamente sicuro memorizzare l’informazione globale che non cambierà attraverso il ciclo di vita del Node come attributo. Nel caso di CycleNode, l’argomento cyclevars non cambia dopo che Node  viene istanziato, quindi non abbiamo bisogno di metterlo in render_context. Ma l’informazione di stato che è specifica del template che si sta presentando, come l’iterazione corrente di CycleNode, dovrebbe essere memorizzata in render_context.

Nota

Nota come abbiamo usato self per dare uno scope alla specifica informazione CycleNode nel render_context. Ci possono essere molteplici CycleNodes in un dato template, quindi abbiamo bisogno di stare attenti a non andare in conflitto con l’informazione di stato di un altro nodo. Il modo più semplice di farlo è usare sempre self come chiave in render_context. Se stai tenendo traccia di diverse variabili di stato, rendi render_context[self] un dizionario.

Registrare il tag

Infine, registra il tag nell’istanza del tuo modulo Library, come spiegato in scrivere tag di template personalizzati qui sopra. Per esempio:

register.tag('current_time', do_current_time)

Il methodo tag() ha bisogni di due argomenti:

  1. Il nome del template tag – una stringa. Se viene tralasciato, il nome della fu..?
  2. La funzione di compilazione –una funzione Python (non il nome di funzione come stringa).

Così come con la registrazione di un filtro, è anche possibile usarlo come decoratore:

@register.tag(name="current_time")
def do_current_time(parser, token):
    ...

@register.tag
def shout(parser, token):
    ...

Se non specifichi l’argomento name, come nel secondo esempio qui sopra, Django userà il nome della funzione come nome del tag.

Passare variabili del template ai tag

Anche se puoi passare un numero arbitrario di argomenti ad un tag di template usando token.split_contents(), gli argomenti sono tutti spacchettati come string literals. E” richiesto un po” più di lavoro per passare un contenuto dinamico (una variabile di template) come argomento ad un tag di template.

Mentre gli esempi precedenti hanno formattato l’ora corrente in una stringa e restituito una stringa, supponi di voler passare un DateTimeField da un oggetto ed avere il template tag che formatta il date-time:

<p>This post was last updated at {% format_time blog_entry.date_updated "%Y-%m-%d %I:%M %p" %}.</p>

All’inizio, token.split_contents() restituirà tre valori:

  1. Il nome tag format_time.
  2. La stringa 'blog_entry.date_updated' (senza le virgolette).
  3. La stringa di formattazione '"%Y-%m-%d %I:%M %p"'. Il valore restituito da split_contents() includerà le virgolette in capo ed in coda agli string literals come questo.

Adesso il tuo tag comincia a sembrare così:

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])

Devi anche cambiare il renderer per ritirare il contenuto attuale della proprietà date_updated dell’oggetto blog_entry. Questo può essere ottenuto usando la classe Variable() in django.template.

Per usare la classe Variable, instanziala con il nome della variabile da risolvere, e poi chiama variable.resolve(context).  Quindi, per esempio:

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 ''

La risoluzione della variabile solleverà una eccezione VariableDoesNotExist se non può risolvere la stringa che gli si passa nel contesto corrente della pagina.

Impostare una variabile nel contesto

L’esempio qui sopra mostra un valore. Generalmente, è tutto più flessibile se i template tag impostano le variabili di template invece di mostrare valori. In questo modo, gli autori di template possono riusare i valori che i tuoi tag di template creano.

Per impostare una variabile nel contesto, usa l’assegnazione di dizionario sull’oggetto context nel metodo render(). Ecco una versione aggiornata di CurrentTimeNode che imposta una variabile di template chiamata current_time invece di mostrarla.

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 ''

Nota che render() restituisce una stringa vuota. render() dovrebbe sempre restituire un output come stringa. Se tutto quel che il tag di template fa è impostare una variabile, render() dovrebbe restituire una stringa vuota.

Ecco come useresti la nuova versione del tag:

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

Scope delle variabili nel contesto

Ogni variabile impostata nel context sarà solo disponibile nello stesso blocco del template in cui è stata assegnata. Questo comportamento è intenzionale; fornisce uno scope per le variabili, in modo che non vadano in conflitto con il context in altri blocchi.

Ma c’è un problema con CurrentTimeNode2: il nome di variabile current_time è hard-coded. Questo significa che dovrai accertarti che il tuo template non usi {{ current_time }} da nessun altra parte, perchè {% current_time %} sovrascriverà ciecamente quel nome di variabile. Una soluzione più pulita è fare in modo che il tag di template specifichi il nome della variabile di output, in questo modo:

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

Per fare ciò, dovrai rifattorizzare sia la funzione di compilazione che la classe Node, così:

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)

La differenza qui è che do_current_time() prende la stringa di formattazione ed il nome di variabile, passandole entrambe a CurrentTimeNode3.

Infine, se hai solo bisogno di avere una sintassi semplice per il tuo template tag personalizzato che aggiorna il context, considera l’utilizzo dello shortcut simple_tag(), che supporta l’assegnamento dei risultati del tag ad una variabile di template.

Analisi fino ad un altro blocco tag

I tag di template possono funzionare in tandem. Per esempio, il tag standard {% comment %} nasconde ogni cosa fino a {% endcomment %}. Per creare un tag di template come questo, usa parser.parse() nella tua funzione di compilazione.

Ecco come potrebbe essere implementato un tag semplificato {% comment %}:

def do_comment(parser, token):
    nodelist = parser.parse(('endcomment',))
    parser.delete_first_token()
    return CommentNode()

class CommentNode(template.Node):
    def render(self, context):
        return ''

Nota

L’implementazione reale di {% comment %} è leggermente diversa nel fatto che permette a template tag rotti di apparire tra {% comment %} e``{% endcomment %}``. Lo fa chiamando parser.skip_past('endcomment') invece di parser.parse(('endcomment',)) seguito da parser.delete_first_token(), evitando la generazione di una node list.

parser.parse() prende una tupla di nomi di tag di blocco «fino a quali fare l’analisi». Restituisce una istanza di django.template.NodeList, che è una lista di tutti gli oggetti Node che il parser ha incontrato «prima» di incontrare un qualsiasi tag presente nella lista.

In "nodelist = parser.parse(('endcomment',))" nell’esempio sopra, nodelist è una lista di nodi tra {% comment %} e {% endcomment %}, senza contare {% comment %} e``{% endcomment %}`` stessi.

Dopo che parser.parse() è stato chiamato, il parser non ha ancora «consumato» il tag {% endcomment %}, quindi il codice ha bisogno di chiamare splicitamente``parser.delete_first_token()``.

CommentNode.render() restituisce una stringa vuota. Ogni cosa tra {% comment %} e {% endcomment %} viene ignorata.

Analizza fino ad un’altro tag, e salva i contenuti

Nell’esempio precedente, do_comment() ha scartato tutto quel che c’era tra {% comment %} e {% endcomment %}. Invece di fare così, è possibile fare qualcosa con il codice che si trova tra i tra i tag.

Per esempio, ecco un template tag personalizzato, {% upper %}, che rende maiuscolo tutto quel che c’è tra sè stesso e {% endupper %}.

Utilizzo:

{% upper %}This will appear in uppercase, {{ your_name }}.{% endupper %}

Come nell’esempio precedente, useremo parser.parse(). Ma questa volta, passeremo la nodelist risultante a 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()

L’unico concetto nuovo qui è self.nodelist.render(context) in UpperNode.render().

Per vedere più esempi di rendering complesso, guarda il codice complesso di {% for %} in django/template/defaulttags.py e {% if %} in django/template/smartif.py.

Back to Top