Γράφοντας δικά σας template tags και φίλτρα

Η γλώσσα template του Django έρχεται με μια μεγάλη ποικιλία από προεγκατεστημένα tags και φίλτρα που έχουν σχεδιαστεί για να αντιμετωπίσουν τις ανάγκες της παρουσίασης της εφαρμογής σας. Παρόλ” αυτά, ίσως χρειαστείτε κάποια λειτουργία η οποία δεν υπάρχει στο βασικό πακέτο των template tags και φίλτρων. Μπορείτε να επεκτείνετε την template μηχανή του Django ορίζοντας δικά σας tags και φίλτρα χρησιμοποιώντας, τι άλλο, Python. Αφού τα δημιουργήσετε μπορείτε να τα χρησιμοποιήσετε στα templates σας αφού πρώτα τα φορτώσετε με το tag {% load %}.

Διάταξη κώδικα

Το πιο κοινό μέρος για να προσδιορίσετε τα δικά σας template tags και φίλτρα είναι μέσα στη Django εφαρμογή σας. Αν σχετίζονται, δε, και με την ήδη υπάρχουσα εφαρμογή, τότε είναι πολύ λογικό να ζουν μέσα στο φάκελο της. Αν όχι, τότε μπορούν να προστεθούν σε κάποια καινούργια εφαρμογή. Αν τα δικά σας template tags και φίλτρα αφορούν γενικά το project ή κάποια άλλη λειτουργία, μπορείτε να φτιάξετε μια νέα εφαρμογή πχ my_utils και να τα αποθηκεύσετε εκεί. Όταν ένα Django app προστίθεται στη λίστα INSTALLED_APPS, οποιοδήποτε tag που είναι ορισμένο στη συμβατική τοποθεσία που περιγράφεται παρακάτω, αυτόματα γίνεται διαθέσιμο για να φορτωθεί μέσα στα templates.

Η εφαρμογή πρέπει να περιέχει ένα φάκελο templatetags, στο ίδιο επίπεδο με τα αρχεία models.py, views.py, κλπ. Αν ο φάκελος δεν υπάρχει ήδη, δημιουργήστε τον – μη ξεχάσετε να προσθέσετε το αρχείο __init__.py για να εξασφαλίσετε ότι ο φάκελος αυτός θα αντιμετωπιστεί ως ένα Python package.

Ο development server δεν θα επανεκκινήσει αυτόματα

Αφού προσθέσετε το module templatetags, θα χρειαστεί να επανεκκινήσετε χειροκίνητα τον server πριν χρησιμοποιήσετε τα tags ή τα φίλτρα στα templates.

Τα δικά σας tags και φίλτρα θα υπάρχουν σε ένα module (όποτε λέμε module εννοούμε ένα φάκελο μαζί με το κενό αρχείο __init__.py μέσα του) μέσα στο templatetags. Το όνομα αυτού του module θα είναι αυτό που θα χρησιμοποιείτε αργότερα, για να φορτώνετε τα tags/φίλτρα, οπότε να είστε προσεκτικοί στην επιλογή του ονόματος ούτως ώστε να μην προκύψουν modules με το ίδιο όνομα σε άλλη εφαρμογή.

Για παράδειγμα, αν τα δικά σας tags/φίλτρα βρίσκονται σε ένα αρχείο με το όνομα poll_extras.py, η δομή των αρχείων της εφαρμογής σας θα δείχνει κάπως έτσι:

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

Και μέσα στο template σας θα μπορείτε να κάνετε διαθέσιμα τα tags/φίλτρα αυτού του αρχείου ως εξής:

{% load poll_extras %}

Η εφαρμογή που περιέχει τα δικά σας tags πρέπει να βρίσκεται μέσα στα INSTALLED_APPS προκειμένου το tag {% load %} να δουλέψει. Αυτό γίνεται για λόγους ασφαλείας: σας επιτρέπει να έχετε Python κώδικα από πολλές template βιβλιοθήκες σε μια μηχανή χωρίς να δοθεί πρόσβαση σε όλες αυτές για κάθε εγκατάσταση του Django.

Δεν υπάρχει όριο στον αριθμό των modules που θα βάλετε μέσα στο templatetags. Θυμηθείτε όμως ότι το tag {% load %} θα φορτώσει τα tags/φίλτρα για το δοθέν όνομα του Python module που βρίσκεται μέσα στο templatetags, όχι το όνομα της εφαρμογής.

Για να είναι μια έγκυρη tag βιβλιοθήκη, το module πρέπει να περιέχει μια μεταβλητή, επιπέδου module, με το όνομα register, ήτοι ένα instance της κλάσης template.Library, με την οποία όλα τα tags και τα φίλτρα θα γίνουν register. Επομένως, στην αρχή του module poll_extras.py, προσθέστε τα ακόλουθα:

from django import template

register = template.Library()

Εναλλακτικά, τα modules του template tag μπορούν να γίνουν register μέσω του argument 'libraries' της κλάσης DjangoTemplates. Αυτό είναι χρήσιμο αν θέλετε να χρησιμοποιήσετε ένα διαφορετικό label από το όνομα του module όταν φορτώνετε τα template tags. Επίσης σας επιτρέπει να κάνετε register τα tags χωρίς την εγκατάσταση κάποια εφαρμογής.

Πίσω από τις σκηνές

Για πάρα πολλά παραδείγματα, διαβάστε τους πηγαίους κώδικες του Django σχετικά με τα προεγκατεστημένα φίλτρα και tags. Βρίσκονται στο django/template/defaultfilters.py και django/template/defaulttags.py, αντίστοιχα.

Για περισσότερες πληροφορίες σχετικά με το tag load, διαβάστε το εγχειρίδιο του.

Γράφοντας δικά σας template φίλτρα

Τα παραμετροποιήσιμα φίλτρα είναι απλώς Python συναρτήσεις οι οποίες δέχονται ένα ή δύο arguments:

  • Την τιμή της μεταβλητής (input) – όχι απαραίτητα τύπου string.
  • Την τιμή του argument (αν υπάρχει) – αυτή μπορεί να είναι μια προεπιλεγμένη τιμή ή να μην υπάρχει καθόλου.

Για παράδειγμα, μέσα στη σύνταξη του φίλτρου {{ var|foo:"bar" }}, το φίλτρο foo θα λάβει μια μεταβλητή var (input) και ένα argument "bar".

Εφόσον η γλώσσα του template δεν παρέχει exception handling, κάθε exception που θα γίνει raised από ένα template filter θα εμφανιστεί ως server error. Επομένως, οι συναρτήσεις των φίλτρων θα πρέπει να αποφεύγουν να κάνουν raise τυχόν exceptions αν υπάρχει κάποια λογική τιμή να επιστραφεί, ως fallback. Σε περίπτωση κάποιου input λόγω του οποίου φαίνεται ξεκάθαρα κάποιο bug στο template, το να κάνετε raise κάποιο exception είναι καλύτερο από το να το αφήσετε απαρατήρητο κάτι το οποίο θα κρύψει το bug.

Παρακάτω φαίνεται ένα παράδειγμα ορισμού ενός φίλτρου:

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

Και εδώ φαίνεται ένα παράδειγμα του πως θα χρησιμοποιηθεί αυτό το φίλτρο στο template:

{{ somevariable|cut:"0" }}

Τα περισσότερα φίλτρα δεν παίρνουν arguments. Σε αυτή την περίπτωση, απλώς αφήστε το εκτός από την συνάρτηση σας. Παράδειγμα:

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

Κάνοντας register τα δικά σας φίλτρα

django.template.Library.filter()

Όταν γράψετε τον ορισμό του φίλτρου σας, θα χρειαστεί να το κάνετε register με το instance της κλάσης Library, για να το κάνετε διαθέσιμο στη γλώσσα template του Django:

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

Η μέθοδος Library.filter() παίρνει δύο ορίσματα:

  1. Το όνομα του φίλτρου – τύπου string.
  2. Τη συνάρτηση του φίλτρου – μια Python συνάρτηση (όχι το όνομα της συνάρτησης ως string).

Μπορείτε, αντ” αυτού, να χρησιμοποιήσετε τη μέθοδο register.filter() ως decorator:

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

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

Αν δεν περάσετε το όρισμα name, όπως στο δεύτερο παράδειγμα παραπάνω, το Django θα χρησιμοποιήσει το όνομα της συνάρτησης ως το όνομα του φίλτρου.

Εν τέλει, η μέθοδος register.filter() δέχεται τρία keyword arguments, το is_safe, το needs_autoescape και το expects_localtime. Αυτά τα arguments περιγράφονται στα φίλτρα και auto-escaping και στα φίλτρα και ζώνες ώρας παρακάτω.

Template φίλτρα που περιμένουν strings

django.template.defaultfilters.stringfilter()

Αν γράφετε ένα template φίλτρο το οποίο περιμένει ένα argument τύπου string ως πρώτο argument, θα πρέπει να χρησιμοποιήσετε τον decorator stringfilter. Αυτό θα μετατρέψει το object στην string τιμή του πριν περαστεί στη συνάρτηση σας:

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

register = template.Library()

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

Με αυτό τον τρόπο, θα μπορείτε να περνάτε, ας πούμε, έναν integer σε αυτό το φίλτρο και αυτό δεν θα προκαλέσει κάποιο AttributeError (επειδή οι integers δεν έχουν την μέθοδο lower()).

Φίλτρα και auto-escaping

Όταν γράφετε ένα δικό σας φίλτρο, σκεφτείτε λιγάκι το πως θα αλληλεπιδρά με την auto-escaping συμπεριφορά του Django. Όταν λέμε auto-escaping εννοούμε τη δυνατότητα αυτόματης αποφυγής εμφάνισης συγκεκριμένων χαρακτήρων-λέξεων οι οποίοι αν εμφανιζόντουσαν θα προκαλούσαν, ενδεχομένως, πρόβλημα στο rendering της σελίδας. Σημειώστε ότι υπάρχουν τρεις τύποι strings οι οποίοι μπορούν να περαστούν μέσα σε κώδικα template:

  • Σκέτα strings: είναι τα παραδοσιακά Python strings, τύπου str ή unicode. Στην έξοδο, αυτά τα string γίνονται escaped αν το auto-escaping είναι ενεργοποιημένο, ειδάλλως παρουσιάζονται όπως είναι.

  • Ασφαλή strings: είναι τα strings τα οποία έχουν μαρκαριστεί ως ασφαλή από περαιτέρω escaping κατά την παρουσίαση τους. Όλα τα απαραίτητα escapings έχουν γίνει μέχρι εδώ. Χρησιμοποιούνται συνήθως για έξοδο η οποία περιέχει σκέτη HTML και προορίζεται να μεταφραστεί ως έχει από τη μεριά του client.

    Εσωτερικώς, αυτά τα strings είναι τύπου SafeBytes ή SafeText. Μοιράζονται μια κοινή κλάση της SafeData, με αποτέλεσμα να μπορείτε να κάνετε τεστ σε αυτά με κώδικα όπως:

    if isinstance(value, SafeData):
        # Do something with the "safe" string.
        ...
    

Ο κώδικας ενός template φίλτρου εμπίπτει σε μια από τις δύο ακόλουθες καταστάσεις:

  1. Your filter does not introduce any HTML-unsafe characters (<, >, ', " or &) into the result that were not already present. In this case, you can let Django take care of all the auto-escaping handling for you. All you need to do is set the is_safe flag to True when you register your filter function, like so:

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

    Αυτό το flag λέει στο Django ότι αν ένα «ασφαλές» string περαστεί στο φίλτρο σας, το αποτέλεσμα θα εξακολουθήσει να είναι «ασφαλές» ακόμη και αν ένα μη-ασφαλές string περαστεί, το Django, αυτόματα θα το κάνει escape, εάν χρειαστεί.

    Μπορείτε να το σκεφτείτε αλλιώς: «αυτό το φίλτρο είναι ασφαλές – δεν υπάρχει πιθανότητα τυχόν μη ασφαλούς HTML».

    Ο λόγος που το flag is_safe είναι απαραίτητο είναι επειδή υπάρχουν πολλών ειδών λειτουργίες για τα strings οι οποίες θα μετατρέψουν ένα object τύπου SafeData πίσω σε ένα κανονικό object τύπου str ή unicode (παρά να προσπαθήσουν να πιάσουν όλα τα strings κάτι το οποίο είναι δύσκολο) και το Django θα επιδιορθώσει τυχόν ζημιά αφού το φίλτρο έχει ολοκληρώσει τις εργασίες του.

    Για παράδειγμα, ας υποθέσουμε ότι έχετε ένα φίλτρο το οποίο προσθέτει το string xx στο τέλος κάθε input. Επειδή αυτό δεν εισάγει επικινδύνους HTML χαρακτήρες στο αποτέλεσμα (εκτός αυτού που είναι ήδη παρόν), θα πρέπει να μαρκάρετε το φίλτρο σας με το is_safe:

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

    Όταν αυτό το φίλτρο χρησιμοποιείται σε ένα template όπου το auto-escaping είναι ενεργοποιημένο, το Django θα κάνει escape την έξοδο οποτεδήποτε το input δεν είναι μαρκαρισμένο ως «ασφαλές».

    Από προεπιλογή, το flag is_safe είναι False και μπορείτε να το παραλείψετε από τα φίλτρα όπου δεν χρειάζεται.

    Να είστε προσεκτικοί όταν αποφασίζετε αν το φίλτρο σας αφήνει όντως τα safe strings ως safe. Εάν αφαιρείτε χαρακτήρες, μπορεί ακούσια να αφήσετε μη-ισορροπημένα HTML tags (χωρίς το start ή το end tag) ή οντότητες στο αποτέλεσμα. Για παράδειγμα, όταν αφαιρείτε ένα > από το input μπορεί να μετατρέψει ένα <a> σε <a, το οποίο θα χρειαστεί να γίνει escaped στην έξοδο για την αποφυγή τυχόν προβλημάτων. Ομοίως, η αφαίρεση του ; μπορεί να μετατρέψει το &amp; σε &amp, η οποία δεν είναι πλέον μια έγκυρη οντότητα και επομένως χρειάζεται περαιτέρω escaping. Οι περισσότερες περιπτώσεις δεν θα είναι τόσο πολύπλοκες αλλά προσέχετε για τέτοιου είδους προβλήματα όταν επανεξετάζετε τον κώδικα σας.

    Μαρκάροντας ένα φίλτρο ως is_safe θα αναγκάσει την επιστρεφόμενη τιμή του φίλτρου να γίνει ένα string. Αν το φίλτρο σας θα πρέπει να επιστρέψει μια τιμή τύπου boolean ή κάποια άλλη πέρα από string, μαρκάροντας το ως is_safe θα έχει πιθανόν ακούσιες συνέπειες (όπως τη μετατροπή της τιμής False τύπου boolean στο string “False”).

  2. Εναλλακτικά, ο κώδικας του φίλτρου σας μπορεί να χειριστεί χειροκίνητα, τυχόν αναγκαίο escaping. Αυτό είναι απαραίτητο όταν εισάγετε καινούργιο HTML κώδικα στο αποτέλεσμα σας. Θα θέλετε να μαρκάρετε την έξοδο ως ασφαλή από περαιτέρω escaping ούτως ώστε ο HTML κώδικας να μην γίνει περαιτέρω escape, οπότε θα χρειαστεί εσείς να χειριστείτε το input.

    Για να μαρκάρετε την έξοδο ως ασφαλή string, χρησιμοποιήστε τη συνάρτηση django.utils.safestring.mark_safe().

    Προσέχετε όμως. Θα χρειαστεί να κάνετε παραπάνω πράγματα από το να μαρκάρετε μόνο την έξοδο ως ασφαλή. Θα χρειαστεί να εξασφαλίσετε ότι είναι όντως ασφαλή και ότι κάνετε εξαρτάται από το αν το auto-escaping είναι ενεργοποιημένο ή όχι. Η ιδέα είναι να γράφετε φίλτρα τα οποία μπορούν να δρουν σε templates όπου το auto-escaping είναι είτε ενεργοποιημένο ή όχι, προκειμένου να κάνετε ευκολότερη τη δουλειά για τους συντάκτες των templates.

    Για να ξέρει το φίλτρο σας την κατάσταση του auto-escaping, θέστε το flag needs_autoescape σε True όταν κάνετε register το φίλτρο σας. (Αν δεν θέσετε αυτό το flag, η προεπιλεγμένη του τιμή είναι False). Αυτό το flag λέει στο Django ότι η συνάρτηση του φίλτρου σας δέχεται ένα επιπλέον keyword argument, με το όνομα autoescape, δηλαδή True αν το auto-escaping είναι ενεργοποιημένο και False αν όχι. Προτείνεται να θέσετε την προεπιπλεγμένη τιμή της παραμέτρου autoescape σε True, ούτως ώστε αν καλέσετε τη συνάρτηση του φίλτρου σας από τον Python κώδικα σας, το φίλτρο σας θα έχει το escaping ενεργοποιημένο από προεπιλογή.

    Για παράδειγμα, ας γράψουμε ένα φίλτρο το οποίο δίνει έμφαση στον πρώτο χαρακτήρα ενός string:

    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)
    

    Tο flag needs_autoescape και το keyword argument autoescape σημαίνουν ότι η συνάρτηση μας θα ξέρει αν το automatic escaping είναι ενεργοποιημένο ή όχι όταν το φίλτρο μας τρέξει. Χρησιμοποιούμε το autoescape για να αποφασίσουμε αν τα input data (το string που πέρασε στο φίλτρο μέσα από την HTML) θα χρειαστεί να περάσουν μέσα από την μέθοδο django.utils.html.conditional_escape ή όχι. (Στην τελευταία περίπτωση χρησιμοποιούμε μια lambda συνάρτηση ως την «escape» συνάρτηση.) Η συνάρτηση conditional_escape() είναι σαν την escape() αλλά κάνει escape το input το οποίο δεν είναι instance της κλάσης SafeData. Αν ένα instance μιας κλάσης SafeData περαστεί στη συνάρτηση conditional_escape(), τα δεδομένα θα επιστραφούν ανέπαφα.

    Στο παραπάνω παράδειγμα, θυμόμαστε να μαρκάρουμε το αποτέλεσμα ως ασφαλή ούτως ώστε η HTML που θα εισαχθεί απ” ευθείας μέσα στο template να μην υποστεί περαιτέρω escaping.

    Δεν υπάρχει λόγος να ανησυχούμε για το flag is_safe σε αυτή την περίπτωση (παρόλου που το περιλαμβάνουμε δεν βλάπτει κανέναν). Οποτεδήποτε χειρίζεστε χειροκίνητα τα θέματα του auto-escaping και επιστρέφετε ένα ασφαλή string, το flag is_safe δεν θα αλλάξει τίποτα με τον ένα ή τον άλλο τρόπο.

Προειδοποίηση

Αποφεύγοντας τα τρωτά σημεία του XSS όταν επαναχρησιμοποιείτε τα προεγκατεστημένα φίλτρα του Django

Τα προεγκατεστημένα φίλτρα του Django έχουν το autoescape=True από προεπιλογή, προκειμένου να έχουν την κατάλληλη autoescaping συμπεριφορά και για να αποφύγουν το τρωτό σημείο ενός cross-site script.

Σε παλαιότερες εκδόσεις του Django, να είστε προσεκτικοί όταν χρησιμοποιείτε τα προεγκατεστημένα φίλτρα του Django επειδή η προεπιλεγμένη τιμή του keyword argument autoescape είναι None. Θα χρειαστεί να το δηλώσετε ως autoescape=True για να έχετε autoescaping.

Για παράδειγμα, αν θέλετε να γράψετε ένα δικό σας φίλτρο με το όνομα urlize_and_linebreaks το οποίο συνδυάζει το φίλτρο urlize με linebreaksbr, τότε το φίλτρο σας θα έδειχνε κάπως έτσι:

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
    )

Μετά το:

{{ comment|urlize_and_linebreaks }}

θα ήταν ισοδύναμο με αυτό:

{{ comment|urlize|linebreaksbr }}

Φίλτρα και ζώνες ώρας

Αν γράφετε δικά σας φίλτρα τα οποία δρουν σε objects του τύπου datetime, συνήθως θα το κάνετε register με το flag expects_localtime ως True:

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

Όταν αυτό το flag είναι True, αν η πρώτη παράμετρος του φίλτρου σας είναι ένα datetime με επίγνωση της ζώνης ώρας (time zone aware datetime), το Django θα το μετατρέψει στην τρέχουσα ζώνη ώρας, κατά περίπτωση, πριν το περάσει στο φίλτρο σας, σύμφωνα με τους κανόνες μετατροπής ζωνών ώρας στα templates.

Γράφοντας δικάς σας template tags

Τα tags είναι περισσότερο πολύπλοκα από τα φίλτρα, επειδή τα tags μπορούν να κάνουν τα πάντα. Το Django παρέχει έναν αριθμό από συντομεύσεις που κάνει τη συγγραφή των περισσότερων τύπων tags ευκολότερη. Πρώτα θα εξερευνήσουμε αυτές τις συντομεύσεις και ύστερα θα εξηγήσουμε πως να γράψετε ένα tag από την αρχή για τις περιπτώσεις που κάποια συντόμευση δεν σας καλύπτει.

Απλά tags

django.template.Library.simple_tag()

Πολλά template tags παίρνουν έναν αριθμό από παραμέτρους – strings ή μεταβλητές template – και επιστρέφουν ένα αποτέλεσμα αφού κάνουν κάποια επεξεργασία βασιζόμενα στις παραμέτρους και σε τυχόν εξωτερικές πληροφορίες. Για παράδειγμα, ένα tag με το όνομα current_time μπορεί να δεχτεί ένα string μορφοποίησης και να επιστρέψει την ώρα ως string, μορφοποιημένη σύμφωνα με αυτό που περάστηκε.

Για να διευκολύνουμε τη δημιουργία αυτού του τύπου των tags, το Django παρέχει μια βοηθητική συνάρτηση, τη simple_tag. Αυτή η συνάρτηση, η οποία είναι μέθοδος της κλάσης django.template.Library, δέχεται μια συνάρτηση η οποία δέχεται παραμέτρους ανεξαρτήτως αριθμού, την κάνει wrap σε μια render συνάρτηση και άλλων απαραίτητων στοιχείων που αναφέρθηκαν παραπάνω και την κάνει register με το σύστημα του template.

Η συνάρτηση μας 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:

  • Ο έλεγχος του αριθμού των απαιτούμενων παραμέτρων κλπ, έχει ήδη γίνει τη στιγμή που η συνάρτηση μας καλείται, οπότε δεν θα χρειαστεί να γίνει κάτι τέτοιο.
  • Τα εισαγωγικά (”” ή '') γύρω από την παράμετρο (αν υπάρχουν) έχουν ήδη αποκοπεί, οπότε έχουμε να κάνουμε με ένα απλό string μέσα στη συνάρτηση του template tag.
  • Αν η παράμετρος ήταν μια μεταβλητή template, τότε η συνάρτηση μας θα περνούσε την τιμή της μεταβλητής και όχι την ίδια τη μεταβλητή.

Σε αντίθεση με λειτουργίες άλλων tags, το simple_tag περνάει την έξοδο του μέσα από τη συνάρτηση conditional_escape() αν το template context είναι σε λειτουργία autoescape, για να εξασφαλίσει σωστή HTML και για να σας προστατέψει από τρωτά σημεία του XSS.

Αν δεν επιθυμείτε κάποιο πρόσθετο escaping, θα χρειαστεί να χρησιμοποιήσετε τη συνάρτηση mark_safe() αν είστε απόλυτα σίγουροι ότι ο κώδικας σας δεν είναι ευάλωτος σε XSS επιθέσεις. Για το χτίσιμο μικρών HTML αποσπασμάτων, η χρήση της συνάρτησης format_html() προτείνεται έναντι της mark_safe().

Αν το template tag χρειάζεται να έχει πρόσβαση στο τρέχων template context, μπορείτε να χρησιμοποιήσετε την παράμετρο takes_context όταν κάνετε register το 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)

Σημειώστε ότι η πρώτη παράμετρος πρέπει να ονομαστεί context.

Για περισσότερες πληροφορίες σχετικά με τη λειτουργία της παραμέτρου takes_context, δείτε στην ενότητα inclusion tags.

Αν χρειαστεί να μετονομάσετε το tag σας, μπορείτε να του δώσετε ένα δικό σας όνομα:

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

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

Οι συναρτήσεις simple_tag μπορούν να δεχτούν απεριόριστο αριθμό από positional ή keyword arguments. Για παράδειγμα:

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

Μετά, στο template, όλα τα arguments διαχωρισμένα με κενά (space character) μπορούν να περαστούν στο template tag. Όπως και στην Python, οι τιμές των keyword arguments ορίζονται με το σύμβολο της ισότητας («=») και πρέπει να δηλωθούν μετά τα positional arguments. Για παράδειγμα:

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

Είναι πιθανόν να αποθηκεύσετε το αποτέλεσμα ενός tag σε μια template μεταβλητή παρά να την εμφανίζετε απ” ευθείας στο template σας. Αυτό γίνεται χρησιμοποιώντας το argument as ακολουθούμενο από το όνομα της template μεταβλητής όπου και θα αποθηκευτεί. Με αυτό τον τρόπο μπορείτε να εκτυπώσετε την τιμή αυτή οπουδήποτε αλλού στο template σας ή και με κάποια επιπλέον επεξεργασία (είτε σε αλλο tag ή φίλτρο):

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

Inclusion tags

django.template.Library.inclusion_tag()

Ένας άλλο κοινός τύπος ενός template tag είναι ο τύπος που εμφανίζει κάποια δεδομένα κάνοντας rendering ένα άλλο template. Για παράδειγμα, το admin interface του Django χρησιμοποιεί δικά του template tags για να εμφανίζει τα κουμπιά στο κάτω μέρος της σελίδας της φόρμας που προσθέτετε ή αλλάζετε κάποιο object. Αυτά τα κουμπιά δείχουν παντού το ίδιο, αλλά το target του link αλάζει ανάλογα το object που επεξεργάζεται εκείνη τη στιγμή – αποτελούν λοιπόν μια τέλεια περίπτωση για τη χρήση μικρών template το οποίο είναι γεμάτο λεπτομέρειες από το τρέχων object. (Στην περίπτωση του admin, το tag ονομάζεται submit_row.)

Αυτού του είδους τα tags ονομάζονται «inclusion tags».

Η συγγραφή των inclusion tags φαίνεται καλύτερα μέσα από ένα παράδειγμα. Ας γράψουμε ένα tag το οποίο εξάγει μια λίστα από επιλογές για ένα δοθέν Poll object, το οποίο δημιουργήθηκε μέσα από το δεύτερο μέρος του οδηγού μας. Θα χρησιμοποιήσουμε το tag ως εξής:

{% show_results poll %}

…και η έξοδος του θα μοιάζει κάπως έτσι:

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

Πρώτα, πρέπει να ορίσετε τη συνάρτηση η οποία παίρνει το argument και επιστρέφει ένα dictionary από δεδομένα. Το σημαντικό εδώ είναι ότι το μόνο που χρειάζεται να επιστρέψουμε είναι ένα dictionary, όχι κάτι περίπλοκο. Αυτό θα χρησιμοποιηθεί ως template context για το κομμάτι του template. Παράδειγμα:

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

Επόμενο βήμα είναι να δημιουργήσετε το template το οποίο θα κάνει render την έξοδο του tag. Αυτό το template είναι ένα μόνιμο χαρακτηριστικό του tag: το προσδιορίζει ο συντάκτης του tag και όχι ο σχεδιαστής του template. Εν ακολουθία του παραδείγματος μας, το template είναι πολύ απλό:

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

Τώρα, δημιουργήστε και κάντε register το inclusion tag καλώντας τη μέθοδο inclusion_tag() σε ένα object της κλάσης Library. Ακολουθώντας το παράδειγμα μας, αν το παραπάνω template βρίσκεται σε ένα αρχείο με το όνομα results.html σε κάποιο φάκελο που είναι ανιχνεύσιμος από τον template loader, θα κάναμε register το tag κάπως έτσι:

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

Εναλλακτικά μπορείτε να κάνετε register το inclusion tag χρησιμοποιώντας ένα instance της κλάσης :class:`django.template.

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

…όταν πρωτο-δημιουργείτε τη συνάρτηση.

Μερικές φορές, τα inclusion tags μπορεί να χρειάζονται έναν μεγάλο αριθμό από παραμέτρους, κάνοντας δύσκολο για τους συντάκτες των templates να περάσουν όλες τις παραμέτρους και να θυμούνται τη σειρά τους. Για την αντιμετώπιση αυτού του προβλήματος, το Django προσφέρει την επιλογή takes_context τύπου boolean για inclusion tags. Αν τη θέσετε ``True `` όταν δημιουργείτε τη συνάρτηση, το tag δεν θα έχει απαιτούμενες παραμέτρους και η Python συνάρτηση του tag θα έχει μόνο μια παράμετρο – το template context όπως είναι όταν κλήθηκε το tag.

Για παράδειγμα, ας υποθέσουμε ότι γράφετε ένα inclusion tag το οποίο θα χρησιμοποιείται πάντα σε κάποιο context που περιέχει τις μεταβλητές home_link και home_title οι οποίες θα οδηγούν πίσω στην αρχική σελίδα. Η Python συνάρτηση του tag θα δείχνει κάπως έτσι:

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

Σημειώστε ότι η πρώτη παράμετρος της συνάρτησης πρέπει να ονομάζεται context.

Στη γραμμή register.inclusion_tag(), προσδιορίσαμε το keyword argument takes_context=True και το όνομα του template. Το template link.html θα δείχνει κάπως έτσι:

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

Έπειτα, κάθε φορά που θέλετε να χρησιμοποιήσετε αυτό το tag, φορτώστε τη βιβλιοθήκη ({% load <όνομα_αρχείου> %}) και καλέσετε τη χωρίς ορίσματα, ως εξής:

{% jump_link %}

Σημειώστε ότι, όταν χρησιμοποιείτε το takes_context=True, δεν χρειάζεται να περάσετε παραμέτρους μέσα στο template tag. Αυτόματα, έχει πρόσβαση στο template context.

Η προεπιλεγμένη τιμή της παραμέτρου takes_context είναι False. Όταν τη θέτετε True, περνάει το context object μέσα στο tag, όπως σε αυτό το παράδειγμα. Αυτή είναι η μόνη διαφορά μεταξύ αυτής της περίπτωσης και του προηγούμενου inclusion_tag παραδείγματος.

Οι συναρτήσεις των inclusion_tag μπορούν να δεχτούν αναρίθμητα positional ή keyword arguments. Για παράδειγμα:

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

Μετά, στο template, όλα τα arguments διαχωρισμένα με κενά (space character) μπορούν να περαστούν στο template tag. Όπως και στην Python, οι τιμές των keyword arguments ορίζονται με το σύμβολο της ισότητας («=») και πρέπει να δηλωθούν μετά τα positional arguments. Για παράδειγμα:

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

Assignment tags

django.template.Library.assignment_tag()

Αποσύρθηκε στην έκδοση 1.9: Το simple_tag θα πρέπει να χρησιμοποιείται καθώς μπορεί πλέον να αποθηκεύει αποτελέσματα σε μια μεταβλητή template.

Για την διευκόλυνση της δημιουργίας μια μεταβλητής στο template context μέσω των tags, το Django παρέχει μια βοηθητική συνάρτηση, την assignment_tag. Αυτή η συνάρτηση δουλεύει με τον ίδιο τρόπο όπως η μέθοδος simple_tag() με τη μόνη διαφορά ότι η assignment_tag αποθηκεύει το αποτέλεσμα του tag σε μια μεταβλητή του context αντί να την εκτυπώνει απ” ευθείας στο template.

Η προηγούμενη συνάρτηση current_time θα μπορούσε, επομένως, να γραφτεί ως:

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

Μπορείτε πλέον να αποθηκεύσετε το αποτέλεσμα σε μια μεταβλητή του template χρησιμοποιώντας το argument as ακολουθούμενο από το όνομα της μεταβλητής. Έπειτα μπορείτε να εκτυπώσετε την τιμή της μεταβλητής οπουδήποτε μέσα στο template επιθυμείτε:

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

Παραμετροποιήσιμα template tags για προχωρημένους

Μερικές φορές τα βασικά χαρακτηριστικά για τη δημιουργία του δικού σας template tag δεν είναι αρκετά. Μην ανησυχείτε, το Django σας δίνει πλήρη πρόσβαση στα εσωτερικά που απαιτούνται για να φτιάξετε ένα template tag από την αρχή.

Μια γρήγορη ματιά

Το σύστημα του template λειτουργεί ακολουθώντας μια διαδικασία δύο σταδίων: compiling και rendering. Για να ορίσετε ένα δικό σας template tag, πρέπει να προσδιορίσετε πως δουλεύει το compilation και πως το rendering.

Όταν το Django συντάσσει (compiles) ένα template, χωρίζει το κείμενο του template σε μια λίστα από “”nodes”“. Κάθε node είναι ένα instance της κλάσης django.template.Node και έχει μια μέθοδο render(). Ένα compiled template είναι, απλώς, μια λίστα από objects τύπου Node. Όταν καλείτε τη μέθοδο render() σε ένα compiled template object, το template καλεί τη μέθοδο render() για καθένα Node που βρίσκεται στη λίστα του, με το δοθέν context. Τα αποτελέσματα ενώνονται όλα μεταξύ τους για να συνθέσουν την έξοδο του template.

Επομένως, για να ορίσετε ένα δικό σας template tag, προσδιορίζετε πως ένα template tag μετατρέπεται σε ένα Node (η συνάρτηση του compilation) και τι κάνει η μέθοδος render() του node.

Γράφοντας τη συνάρτηση του compilation

Για κάθε template tag που βρίσκει ο template parser, καλείται μια Python συνάρτηση με τα περιεχόμενα του tag και το ίδιο το parser object. Αυτή η συνάρτηση είναι υπεύθυνη να επιστρέψει ένα instance του Node βασιζόμενη στα περιεχόμενα του tag.

Για παράδειγμα, ας γράψουμε μια πλήρης υλοποίηση του απλού μας template tag, {% current_time %}, το οποίο εμφανίζει την τρέχουσα ημερομηνία/ώρα, μορφοποιημένη σύμφωνα με τις παραμέτρους (σύνταξη func:~time.strftime) που δόθηκαν στο tag. Είναι καλή ιδέα να αποφασίσετε τη σύνταξη του tag πριν από οτιδήποτε άλλο. Στη δικιά μας περίπτωση, ας πούμε ότι το tag θα πρέπει να χρησιμοποιείται κάπως έτσι:

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

Ο parser για αυτή τη συνάρτηση θα πρέπει να παίρνει την παράμετρο και να δημιουργεί ένα object του 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])

Σημειώσεις

  • Η παράμετρος parser είναι ένα template parser object. Δεν το χρειαζόμαστε σε αυτό το παράδειγμα.
  • Το token.contents είναι ένα string των περιεχομένων του tag. Στο δικό μας παράδειγμα, είναι το 'current_time "%Y-%m-%d %I:%M %p"'.
  • Η μέθοδος token.split_contents() διαχωρίζει, με βάση τον χαρακτήρα του διαστήματος, τις παραμέτρους ενώ παράλληλα κρατάει τα strings, ανέπαφα, με τον χαρακτήρα που έχουν δηλωθεί (’...’ ή ”...”). Αν χρησιμοποιούνταν η token.contents.split() τότε θα διαχώριζε όλα τα περιεχόμενα του tag με βάση το χαρακτήρα του διαστήματος, συμπεριλαμβανομένων και των κενών που υπάρχουν στο string. Αυτό βέβαια δεν είναι βολικό. Καλό είναι, λοιπόν, να χρησιμοποιείτε τη μέθοδο token.split_contents().
  • Η συνάρτηση που φτιάξαμε, είναι υπεύθυνη να κάνει raise το exception django.template.TemplateSyntaxError, με βοηθητικά μηνύματα για τυχόν συντακτικά λάθη.
  • Τα exceptions τύπου TemplateSyntaxError χρησιμοποιούν τη μεταβλητή tag_name. Μην γράφετε μόνοι σας το όνομα του tag στα μηνύματα σφάλματος, επειδή αυτό δεσμεύει το όνομα του tag με τη συνάρτηση σας. Το token.contents.split()[0] θα περιέχει “”πάντα”” το όνομα του tag σας – ακόμη και αν το tag δεν έχει παραμέτρους.
  • The function returns a CurrentTimeNode with everything the node needs to know about this tag. In this case, it just passes the argument – "%Y-%m-%d %I:%M %p". The leading and trailing quotes from the template tag are removed in format_string[1:-1].
  • Το parsing γίνεται σε πολύ χαμηλό επίπεδο. Οι Django developers έχουν πειραματιστεί στο να γράφουν μικρά frameworks πάνω σε αυτό το σύστημα parsing, χρησιμοποιώντας τεχνικές όπως οι γραμματικές EBNF (EBNF grammars), αλλά αυτά τα πειράματα έκαναν την μηχανή του template πολύ αργή. Γίνεται, λοιπόν, σε χαμηλό επίπεδο επειδή αυτό είναι γρηγορότερο.

Γράφοντας τον renderer

Το δεύτερο βήμα για τη δημιουργία δικών μας tags είναι ο ορισμός της subclass της Node η οποία έχει μια μέθοδο render().

Συνεχίζοντας το παραπάνω παράδειγμα, χρειάζεται να ορίσουμε το 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)

Σημειώσεις

  • Η μέθοδος __init__() λαμβάνει τη μεταβλητή format_string από τη συνάρτηση do_current_time(). Πάντα να περνάτε τυχόν options/parameters/arguments σε ένα Node μέσω της __init__() μεθόδου του.
  • Η μέθοδος render() είναι εκεί όπου γίνονται όλα.
  • Η render() θα πρέπει γενικά να αποτυγχάνει σιωπηρά, ειδικά σε παραγωγικό περιβάλλον (production environment). Ωστόσο, σε μερικές περιπτώσεις, ειδικά αν το context.template.engine.debug είναι True, αυτή η μέθοδος θα πρέπει να κάνει raise ένα exception για να κάνει το debugging ευκολότερο. Για παράδειγμα, πολλά προεγκατεστημένα tags του Django κάνουν raise το django.template.TemplateSyntaxError αν λάβουν λάθος αριθμό ή τύπο παραμέτρων.

Τελικώς, αυτή η αποσύνδεση μεταξύ του compilation και του rendering έχει ως αποτέλεσμα ένα αποδοτικό σύστημα template, επειδή ένα template μπορεί να κάνει render πολλαπλά contexts χωρίς να χρειαστεί να γίνει parsed πολλές φορές.

Εκτιμήσεις σχετικά με το auto-escaping

Η έξοδος των template tags δεν περνούν αυτόματα μέσα από auto-escaping φίλτρα (εκτός της μεθόδου simple_tag(), όπως περιγράφηκε παραπάνω). Ωστόσο, υπάρχουν ακόμη μερικά πράγματα που πρέπει να έχετε στο νου σας όταν γράφετε ένα template tag.

Αν η συνάρτηση render() του template σας αποθηκεύει το αποτέλεσμα σε μια context variable (αντί να επιστρέφει το αποτέλεσμα σε ένα string), θα πρέπει να φροντίσει να καλέσει την mark_safe(), αν αυτό είναι απαραίτητο. Όταν η μεταβλητή γίνει, τελικώς, rendered (εμφανιστεί-τυπωθεί στο template, αν θέλετε), θα επηρεαστεί από τη ρύθμιση του auto-escape (αναλόγως αν είναι ενεργοποιημένη ή όχι), οπότε το περιεχόμενο που πρέπει να είναι safe από περαιτέρω escaping χρειάζεται να μαρκαριστεί.

Επίσης, αν το template tag σας δημιουργεί ένα καινούργιο context για να εκτελέσει κάποιο sub-rendering, θέστε το attribute του auto-escape στην τιμή του τρέχοντος context. Η μέθοδος __init__ για την κλάση Context δέχεται μια παράμετρο με το όνομα autoescape, την οποία μπορείτε να χρησιμοποιήσετε για αυτό το σκοπό. Για παράδειγμα:

from django.template import Context

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

Αυτό δεν είναι μια συνηθισμένη περίπτωση, αλλά είναι χρήσιμο να το ξέρετε αν κάνετε rendering ένα template εσείς οι ίδιοι.

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

Αν δεν περνάγαμε την τιμή του context.autoescape στο νέο μας Context σε αυτό το παράδειγμα, τα αποτελέσματα θα γινόντουσαν πάντα αυτόματα escaped, κάτι το οποίο μπορεί να μην ήταν η επιθυμητική συμπεριφορά αν το template tag χρησιμοποιούταν μέσα στο block tag {% autoescape off %}.

Εκτιμήσεις σχετικά με την ασφάλεια των threads

Όταν ένα node γίνει parsed, η render μέθοδος του μπορεί να καλεστεί πολλές φορές. Επειδή το Django, μερικές φορές, τρέχει σε περιβάλλοντα multi-threaded, ένα απλό node μπορεί ταυτοχρόνως να γίνει rendering με διαφορετικά contexts ως response για δύο ξεχωριστά requests. Επομένως, είναι σημαντικό να εξασφαλίσετε ότι τα template tags σας είναι thread safe.

Για να το εξασφαλίσετε αυτό, δεν θα πρέπει να αποθηκεύετε ποτέ τις πληροφορίες της κατάστασης στο ίδιο το node. Για παράδειγμα, το Django παρέχει ένα προεγκατεστημένο template tag cycle το οποίο ανατρέχει μια λίστα από δοθέντα strings κάθε φορά που γίνεται rendered:

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

Μια απλοϊκή υλοποίηση του CycleNode θα έμοιαζε κάπως έτσι:

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)

Αλλά, ας υποθέσουμε ότι την ίδια χρονική στιγμή, δύο templates κάνουν rendering το κομμάτι του template παραπάνω:

  1. Το Thread 1 πραγματοποιεί την πρώτη επανάληψη του βρόγχου, η μέθοδος CycleNode.render() επιστρέφει “row1”
  2. Το Thread 2 πραγματοποιεί την πρώτη επανάληψη του βρόγχου, η μέθοδος CycleNode.render() επιστρέφει “row2”
  3. Το Thread 1 πραγματοποιεί την δεύτερη επανάληψη του βρόγχου, η μέθοδος CycleNode.render() επιστρέφει “row1”
  4. Το Thread 2 πραγματοποιεί την δεύτερη επανάληψη του βρόγχου, η μέθοδος CycleNode.render() επιστρέφει “row2”

Το CycleNode κάνει την επανάληψη (iterating), αλλά την κάνει καθολικά (globally). Όσον αφορά τα Thread 1 και Thread 2, το καθένα επιστρέφει πάντα την ίδια τιμή. Αυτό, φυσικά, δεν είναι αυτό που θέλουμε!

Για την αποφυγή τέτοιων καταστάσεων, το Django παρέχει ένα render_context το οποίο σχετίζεται με το context του template το οποίο γίνεται εκείνη τη στιγμή rendered. Το render_context συμπεριφέρεται όπως ένα Python dictionary και θα πρέπει να χρησιμοποιείται για να αποθηκεύονται οι καταστάσεις του Node μεταξύ κλήσεων της μεθόδου render.

Ας αλλάξουμε την υλοποίηση της κλάσης CycleNode για να χρησιμοποιεί το 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)

Σημειώστε ότι δεν είναι καθόλου λάθος να αποθηκεύετε ως attribute καθολικές πληροφορίες οι οποίες δεν αλλάζουν κατά τη διάρκεια της ζωής ενός Node. Στην περίπτωση του CycleNode, το argument cyclevars δεν αλλάζει μετά την αρχικοποίηση του Node, οπότε δεν χρειάζεται να το βάλουμε μέσα στο render_context. Αλλά η πληροφορία της κατάστασης η οποία είναι συγκεκριμένη στο template το οποίο γίνεται rendered, όπως η τρέχουσα επανάληψη βρόγχου του CycleNode, θα πρέπει να αποθηκευτεί μέσα στο render_context.

Σημείωση

Σημειώστε το πως χρησιμοποιήσαμε το self για να έχουμε πρόσβαση σε συγκεκριμένη πληροφορία του CycleNode μέσα από το render_context. Μπορεί να υπάρχουν πολλαπλά CycleNodes μέσα σε ένα template, οπότε πρέπει να είμαστε προσεκτικοί να μην αφαιρέσουμε την πληροφορία της κατάστασης άλλου node. Ο πιο εύκολος τρόπος για να επιτευχθεί κάτι τέτοιο είναι πάντα να χρησιμοποιείτε το self ως το key μέσα στο render_context. Αν παρακολουθείτε πολλές ματαβλητές κατάστασης (state variables), κάντε το render_context[self] ένα dictionary.

Κάνοντας register το tag

Τέλος, κάντε register το tag με το instance του module Library, όπως εξηγήθηκε στην ενότητα γράφοντας δικά σας template φίλτρα παραπάνω. Παράδειγμα:

register.tag('current_time', do_current_time)

Η μέθοδος tag() λαμβάνει δύο arguments:

  1. Το όνομα του template tag – ένα string. Αν αυτό παραλειφθεί, θα χρησιμοποιηθεί το όνομα της συνάρτησης του compilation.
  2. Τη συνάρτηση του φίλτρου – μια Python συνάρτηση (όχι το όνομα της συνάρτησης ως string).

Όπως και με το registration των φιλτρών, μπορείτε να χρησιμοποιήσετε έναν decorator:

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

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

Αν παραλείψετε την παράμετρο name, όπως στο δεύτερο παράδειγμα παραπάνω, το Django θα χρησιμοποιήσει το όνομα της συνάρτησης ως το όνομα του tag.

Περνώντας template μεταβλητές στο tag

Όπως γνωρίζετε μπορείτε να περάσετε οποιοδήποτε αριθμό από παραμέτρους σε ένα template tag και χρησιμοποιώντας τη μέθοδο token.split_contents() οι παράμετροι θα είναι διαθέσιμες σε εσάς ως strings. Ωστόσο, χρειάζεται λίγη περισσότερη δουλειά για να περάσετε δυναμικό περιεχόμενο (μια template μεταβλητή) στο template tag ως παράμετρο.

Ενώ τα προηγούμενα παραδείγματα μορφοποιούσαν την τρέχουσα ώρα σε ένα string και επέστρεφαν το string, υποθέστε τώρα ότι θέλετε να περάσετε ένα DateTimeField από ένα object και θέλετε το template tag να μορφοποιήσει αυτό το object (αυτή την ημερομηνία-ώρα, αν θέλετε):

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

Αρχικά, η μέθοδος token.split_contents() θα επιστρέψει τρεις τιμές:

  1. Το όνομα του tag, format_time.
  2. Το string 'blog_entry.date_updated' (χωρίς τα μονά εισαγωγικά).
  3. Το string που θα χρησιμοποιηθεί για την μορφοποίηση '"%Y-%m-%d %I:%M %p"'. Η επιστρεφόμενη τιμή από τη μέθοδο split_contents() θα συμπεριλάβει τα διπλά εισαγωγικά στην αρχή και το τέλος των strings (όπως ακριβώς εισήχθησαν στο tag μέσα στο template).

Τώρα το tag θα αρχίσει να μοιάζει κάπως έτσι:

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

Θα χρειαστεί επίσης να αλλάξετε και τον renderer για να λάβει τα πραγματικά περιεχόμενα του date_updated property του object blog_entry. Αυτό μπορεί να επιτευχθεί χρησιμοποιώντας την κλάση Variable() μέσα στο django.template.

Για να χρησιμοποιήσετε την κλάση Variable, απλώς αρχικοποιήστε τη με το όνομα της μεταβλητής η οποία θα γίνει resolved και μετά καλέστε τη μέθοδο variable.resolve(context). Άρα, για παράδειγμα:

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

Η αναζήτηση της μεταβλητής (variable resolution) θα κάνει throw ένα exception VariableDoesNotExist αν δεν μπορεί να γίνει resolve το string ('blog_entry.date_updated') που περάστηκε στο τρέχων context της σελίδας.

Θέτοντας μια μεταβλητή στο context

Το παραπάνω παράδειγμα απλώς εξάγει μια τιμή. Γενικότερα, είναι περισσότερο ευέλικτο αν τα template tags σας θέτουν template μεταβλητές αντί να εξάγουν (εκτυπώνουν) τις τιμές τους. Με αυτό τον τρόπο, οι συντάκτες των template θα μπορούν να επαναχρησιμοποιούν τις τιμές τις οποίες δημιούργησαν τα template tags σας.

Για να θέσετε μια μεταβλητή στο context, απλώς χρησιμοποιήστε την εκχώρηση της στο dictionary του context object μέσα στη μέθοδο render(). Παρακάτω φαίνεται μια ενημερωμένη έκδοση της κλάσης CurrentTimeNode η οποία θέτει την template μεταβλητή current_time αντί απλώς να την εκτυπώνει:

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

Σημειώστε ότι η μέθοδος render() επιστρέφει ένα κενό string. Η render() θα πρέπει πάντα να επιστρέφει ένα string. Αν το μόνο που κάνει ένα template tag είναι να ορίζει μια μεταβλητή, τότε η render() θα πρέπει να επιστρέφει ένα κενό string.

Παρακάτω φαίνεται πως μπορείτε να χρησιμοποιείτε τη νέα αυτή έκδοση του tag:

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

Το variable scope του context

Οποιαδήποτε μεταβλητή οριστεί στο context, θα είναι διαθέσιμη στο ίδιο block του template στο οποίο ορίστηκε. Αυτή η συμπεριφορά είναι σκόπιμη. Παρέχει ένα scope για μεταβλητές ούτως ώστε να μην συγκρούονται με κάποιο context από άλλα blocks.

Αλλά υπάρχει ένα πρόβλημα με την κλάση CurrentTimeNode2: Το όνομα της μεταβλητής current_time είναι γραμμένο hard-coded. Αυτό σημαίνει ότι πρέπει να σιγουρευτείτε πως το template σας δεν χρησιμοποιεί πουθενά αλλού το {{ current_time }}, επειδή το {% current_time %} θα έκανε overwrite την τιμή της μεταβλητής. Μια πιο καθαρή λύση είναι να κάνετε το template tag να προσδιορίσει ένα όνομα για τη μεταβλητή, ως εξής:

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

Για να το κάνετε αυτό, θα χρειαστεί να αλλάξετε και τη συνάρτηση του compilation και την κλάση Node, ως εξής:

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)

Η διαφορά εδώ είναι ότι η μέθοδος do_current_time() παίρνει το format μορφοποίησης και το όνομα της μεταβλητής και δίνει και τα δύο στην κλάση CurrentTimeNode3.

Τέλος, αν θέλετε μόνο να έχετε μια απλή σύνταξη για το δικό σας template tag που αλληλεπιδρά με το context και το ενημερώνει, σκεφτείτε να χρησιμοποιήσετε τη συντόμευση simple_tag(), η οποία υποστηρίζει τον ορισμό μεταβλητής της εξόδου ενός tag (μέσω της λέξη-κλειδί as, όπως είδαμε παραπάνω).

Κάνοντας parsing μέχρι να βρεθεί κάποιο άλλο block tag

Τα template tags μπορούν να δουλέψουν αλυσιδωτά. Για παράδειγμα, το στάνταρντ tag {% comment %} αγνοεί τα πάντα μέχρι να βρει το tag {% endcomment %}. Για να δημιουργήσετε ένα template tag όπως αυτό, χρησιμοποιήστε τη μέθοδο parser.parse() μέσα στη συνάρτηση του compilation.

Εδώ φαίνεται η υλοποίηση της απλοποιημένης έκδοσης του tag {% 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 ''

Σημείωση

Η υλοποίηση του tag {% comment %} διαφέρει ελαφρώς στο ότι επιτρέπει την εμφάνιση broken template tags μεταξύ του {% comment %} και του {% endcomment %}. Το κάνει αυτό καλώντας τη μέθοδο parser.skip_past('endcomment') αντί της parser.parse(('endcomment',)) ακολουθούμενη από τη μέθοδο parser.delete_first_token(), με αποτέλεσμα την αποφυγή της δημιουργίας μιας λίστας από nodes.

Η μέθοδος parser.parse() παίρνει ένα tuple από ονόματα block tags “”να γίνει parse μέχρι”“. Επιστρέφει ένα instance του django.template.NodeList, το οποίο είναι μια λίστα από Node objects τα οποία βρήκε ο parser “”πριν”” βρει κάποιο από τα tags που είναι μέσα στο tuple.

Στο "nodelist = parser.parse(('endcomment',))" στο παραπάνω παράδειγμα, η nodelist είναι μια λίστα από όλα τα nodes μεταξύ του {% comment %} και του {% endcomment %}, χωρίς τα ίδια τα {% comment %} και {% endcomment %}.

Αφού καλεστεί η parser.parse(), ο parser δεν έχει «καταναλώσει» ακόμα το {% endcomment %} tag, οπότε ο κώδικας χρειάζεται να καλέσει ρητώς τη μέθοδο parser.delete_first_token().

Η μέθοδος CommentNode.render() απλώς επιστρέφει ένα κενό string. Οτιδήποτε μεταξύ του {% comment %} και {% endcomment %} αγνοείται.

Κάνοντας parsing μέχρι να βρεθεί κάποιο άλλο block tag και αποθήκευση περιεχομένων

Στο προηγούμενο παράδειγμα, η συνάρτηση do_comment() απέρριπτε τα πάντα μεταξύ του blck tag {% comment %} και {% endcomment %}, επειδή θεωρούνταν σχόλια και αυτά δεν γινόντουσαν render στην τελική σελίδα που θα έβλεπε ο χρήστης. Αντί να γίνεται αυτό, μπορείτε να κάνετε κάτι με τον κώδικα μεταξύ των block tags.

Για παράδειγμα, εδώ φαίνεται ένα παραμετροποιήσιμο template tag, με το όνομα {% upper %}, το οποίο μετατρέπει σε κεφαλαία γράμματα όλους τους χαρακτήρες μεταξύ του ίδιου και του {% endupper %}.

Χρήση

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

Όπως στο προηγούμενο παράδειγμα, θα χρησιμοποιήσουμε τη μέθοδο parser.parse(). Αλλά αυτή τη φορά, θα περάσουμε τη nodelist στο Node, εν αντιθέσει με την προηγούμενη φορά που επιστρέφαμε το CommentNode() χωρίς κάποιο nodelist μέσα:

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

Η μοναδική καινούργια έννοια εδώ είναι το self.nodelist.render(context) μέσα στη μέθοδο UpperNode.render().

Για περισσότερα παραδείγματα περίπλοκων renderings, δείτε τον πηγαίο κώδικα του tag {% for %} στο αρχείο django/template/defaulttags.py και το tag {% if %} στο αρχείο django/template/smartif.py.

Back to Top