Bagaimana membuat cetakan etiket dan saringan disesuaikan

Cetakan bahasa Django datang dengan beragam luas dari built-in tags and filters dirancang untuk megnalamatkan kebutuhan pembawaan logis dari aplikasi anda. Namun, anda mungkin menemukan diri anda sendiri membutuhkan fungsi yang tidak dicakupi oleh inti kumpulan dari cetakan primitif. Anda dapat memperpanjang cetakan mesin dengan menentukan penyesuaian etiket dan penyaring menggunakan Python, dan membuat mereka tersedua pada cetakan anda menggunakan etiket {% load %}.

Tata letak kode

Tempat paling umum untuk menentukan cetakan etiket dan penyaring adalah didalam aplikasi Django. Jika mereka terhubung pada aplikasi yang ada, dia masuk akal untuk menggabungkan mereka; kalau tidak, mereka dapat ditambahkan ke aplikasi baru. Ketika aplikasi Django ditambahkan ke INSTALLED_APPS, etiket apapun dia tentukan dalam tempat biasa dibawah otomatis dibuat tersedia untuk dimuat dengan cetakan.

Aplikasi harus mengandung sebuah direktori templatetags, pada tingkatan sama seperti models.py, views.py, dll. Jika ini tidak ada, buat dia - jangan lupakan berkas __init__.py untuk memastikan direktori diperlakukan sebagai paket Python.

Peladen pengembangan tidak akan otomatis nyala kembali

Setelah menambahkan modul templatetags, anda akan butuh memulai kembali peladen anda sebelum anda dapat menggunakan etiket atau penyaring dalam cetakan.

Penyesuaian etiket dan penyaring anda akan berada dalam sebuah modul didalam direktori templatetags. Nama dari berkas modul adalah nama anda akan gunakan untuk memuat etiket kemudian, jadi berhati-hatilah untuk mengambil sebuah nama yang tidak akan bentrok dengan penyesuaian etiket dan penyaring di aplikasi lain.

For example, if your custom tags/filters are in a file called poll_extras.py, your app layout might look like this:

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

Dan di cetakan anda, anda akan menggunakan berikut:

{% load poll_extras %}

Aplikasi yang mengandung penyesuaian etiket harus berada dalam INSTALLED_APPS agar etiket {% load %} bekerja. Ini adalah fitur keamanan: Dia mengizinkan anda untuk menempatkan kode Python untuk banyak cetakan pustaka pada sebuah mesin rumah tunggal tanpa mengadakan akses ke semua dari mereka untuk setiap pemasangan Django.

Tidak ada batasan pada bagaimana banyak modul anda taruh dalam paket templatetags. Hanya perlu diingat bahwa sebuah pernyataan {% load %} akan memuatetiket/penyaring untuk nama modul Python yang diberikan, bukan nama dari aplikasi.

Untuk menjadi pustaka sah, modul harus mengandung sebuah variabel tingkat-modul yang dinamai register yaitu sebuah instance template.Library, dimana semua etiket dan penyaring didaftarkan. Jadi dekat diatas modul anda, taruh berikut:

from django import template

register = template.Library()

Kalau tidak, cetakan modul etiket dapat didaftarkan melalui argumen 'libraries' ke DjangoTemplates. Ini berguna jika anda ingin menggunakan label berbeda dari nama cetakan modul etiket ketika memuat cetakan etiket. Dia juga mengadakan anda untuk mendaftar etiket tanpa memasang sebuah aplikasi.

Dibalik layar

Untuk contoh berton-ton, baca kode sumber untuk etiket dan saringan awalan Django. Mereka ada di django/template/defaultfilters.py and django/template/defaulttags.py, masing-masing.

Untuk informasi lebih pada etiket load, baca dokumentasinya.

Menulis saringan cetakan penyesuaian

Penyaring penyesuaian adalah fungsi Python yang mengambil satu atau dua argumen:

  • Nilai dari variabel (masukan) -- tidak perlu deretan karakter.
  • Nilai dari argumen -- ini dapat mempunyai nilai awal, atau ditinggalkan sama sekali.

Sebagai contoh, dalam saringan {{ var|foo:"bar" }}, the filter foo akan dilewatkan variabel var dan argumen ''bar''.

Sejak cetakan bahasa tidak menyediakan penanganan pengecualian, setiap pengecualian yang muncul dari sebuah cetakan saringan akan tidak terlindungi sebagai sebuah kesalahan peladen. Dengan demikian, fungsi penyaring harus menghindari memunculkan pengecualian jika ada alasan nilai mundur untuk kembali. Dalam kasus dari masukan yang mewakili kesalahan jelas dalam sebuah cetakan, menampilkan pencegualian mungkin masih lebih baik daripada kegagalan diam yang menyembunyikan kesalahan.

Ini adalah sebuah contoh pengertian saringan

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

Dan ini adalah sebuah contoh bagaimana saringan itu akan digunakan:

{{ somevariable|cut:"0" }}

Kebanyakan penyaring tidak mengambil argumen. Dalam kasus ini, tinggalkan argumen dari fungsi anda:

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

Mendaftarkan saringan penyesuaian

django.template.Library.filter()

Sekali anda telah menulis arti saringan anda, anda butuh mendaftarkannya dengan instance Library anda, untuk membuatnya tersedia pada bahasa cetakan Django:

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

Cara Library.filter() mengambil dua argumen:

  1. Nama dari saringan -- sebuah deretan karakter.
  2. Fungsi himpunan -- Sebuah fungsi Python (bukan nama fungsi sebagai deretan karakter).

Anda dapat menggunakan register.filter() sebagai penghias:

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


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

Jika anda tidak mengubah argumen name, seperti di contoh kedua di atas, Django akan menggunakan nama fungsi sebagai nama filter.

Akhirnya, register.filter() juga menerima tiga argumen katakunci, is_safe, needs_autoescape, dan expects_localtime. Argumen ini digambarkan dalam penyaring dan pelolosan otomatis dan penyaring dan zona waktu dibawah.

Cetakan penyaring yang mengharapkan deretan karakter

django.template.defaultfilters.stringfilter()

Jika anda sedang menulis sebuah cetakan penyaring yang hanya menerima sebuah deretan karakter sebagai argumen pertama, anda harus menggunakan decorator stringfilter. Ini akan merubah sebuah obyek ke nilai deretan karakternya sebelum dilewatkan ke fungsi anda:

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

register = template.Library()


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

Jalan ini, anda akan dapat melewatkan, katakan, sebuah integer ke penyaring ini, dan dia tidak menyebabkan sebuah AttributeError (karena integer tidak mempunyai cara lower()).

Penyaring dan pelolosan otomatis

Ketika menulis penyesuaian penyaring, berikan beberapa pemikiran bagaimana penyaring akan berinteraksi dengan perilaku pelolosan-otomatis Django. Catat bahwa dua jenis dari string dapat dilewatkan didalam sekitar kode cetakan:

  • Raw strings adalah string Python asli. Pada keluaran, mereka diloloskan jika pelolosan-otomatis dalam pengaruh dan dihadirkan tidak berubah, sebaliknya.

  • Safe strings adalah deretan karakter yang telah ditandai aman dari pelolosan lanjut pada waktu keluar. Apa saja yang diperlukan pelolosan telah dilakukan. Mereka umumnya digunakan untuk keluaran yang mengandung HTML mentah yang akan di ditafsirkan apa adanya pada sisi klien.

    Secara internal, string ini adalah jenis SafeString. Anda dapat mencoba mereka menggunakan kode seperti:

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

Kode cetakan penyaring gagal kedalam satu dari dua keadaan:

  1. Penyaring anda tidak memperkenalkan salah satu karakter tidak aman (<, >, ', " atau &) kedalam hasil yang tidak hadir. Dalam kasus ini, anda dapat membiarkan Django mengambil semua penanganan pelolosan otomatis untuk anda. Semua anda butuh untuk dilakukan adalah menyetel bendera is_safe ke True ketika anda mendaftar fungsi penyaringan, seperti begitu:

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

    Bendera ini memberitahu Django bahwa jika sebuah deretan karakter "safe" dilewatkan kedalam penyaring anda, hasil akan masih menjadi "safe" dan jika sebuah deretan karakter tidak aman dilewatkan, Django akan otomatis meloloskannya, jika diperlukan.

    Anda dapat berpikir dari ini sebagai berarti "penyaring ini aman -- dia tidak memperkenalkan kemungkinan apapun dari HTML tidak aman."

    Alasan is_safe perlu adalah karena ada banyak tindakan-tindakan string biasa yang akan merubah sebuah obyek SafeData kembali ke obyek str biasa dan, daripada mencoba menangkap mereka semua, yang akan sangat sulit, Django memperbaiki kerusakan setelah penyaring lengkap.

    Sebagi contoh, andaikan anda mempunyai sebuah penyaring yang menambahkan deretan karakter xx ke akhir dari masukan. Sejak ini diperkenalkan karakter HTML tidak berbahaya ke hasil (selain dari apapun itu sudah hadir), anda harus menandai penyaring anda dengan is_safe:

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

    Ketika penyaring ini digunakan dalam sebuah cetakan dimana pelolosan otomatis diadakan, Django akan meloloskan keluaran ketika masukan belum ditandai sebagai "safe".

    Secara awalan, is_safe adalah False, dan anda dapat menghilangkannya dari saringan dimana dia tidak wajib.

    Hati-hati ketika memutuskan jika penyaring anda sangat membiarkan deretan karakter aman sebagai safe. Jika anda sedang memidnahkan karakter, anda mungkin secara tidak sengaja membiarkan etiket HTML tidak seimbang atau kesatuan dalam hasil. Sebagai contoh, memindahkan sebuah > dari masukan mungkin mengubah <a> menjadi <a, yang akan butuh diloloskan pada keluaran untuk menghindari masalah. Demikian pula, memindahkan titik koma (;) dapat merubah &amp; into &amp, yang tidak lagi kesatuan sah dan demikian butuh pelolosan. Kebanyakan kasus tidak dekat dengan trik ini, tetapi jaga mata anda untuk masalah apapun seperti itu ketika meninjau kode anda.

    Menandai sebuah penyaring is_safe akan memaksa nilai kembalian penyaring menjadi deretan karakter. Jika penyaring anda harus mengembalikan sebuah boolean atau nilai bukan deretan karakter, tandai dia is_safe akan mungkin mempunyai konsekuensi yang tidak diinginkan (seperti merubah boolean False menjadi deretan karakter 'False').

  2. Kalau tidak, kode penyaring anda dapat secara manual merawat pelolosan apapun yang dibutuhkan. Ini adalah penting ketika anda sedang memperkenalkan markah HTML baru kedalam hasil. Anda ingin menandai keluaran sebagai aman dari pelolosan lebih lanjut sehingga markah HTML anda tidak diloloskan lebih lanjut, jadi anda akan butuh menangani masukan diri anda sendiri.

    Untuk menandai keluaran sebagai deretan karakter aman, gunakan django.utils.safestring.mark_safe().

    Berhati-hatilah, meskipun. Anda butuh melakukan lagi daripada hanya menandai keluaran sebagai aman. Anda butuh memastikan dia sangat adalah aman, dan apa yang anda lakukan tergantung pada apakah pelolosan otomatis berpengaruh. Idenya adalah menulis penyaring yang dapat berjalan dalam cetakan dimana pelolosan otomatis salah satunya nyala atau mati agar membuat hal-hal semakin mudah untuk cetakan penulis anda.

    Agar penyaring anda mengetahui keadaan pelolosan otomatis saat ini, setel bendera needs_autoescape ke True ketika anda mendaftar fungsi penyaring anda. (Jika anda tidak menentukan bendera ini, dia awalnya adalah False). Bendera ini mengatakan Django bahwa fungsi penyaring anda ingin dilewatkan sebuah argumen katakunci tambahan, dipanggil autoescape, yaitu True jika pelolosan otomatis dalam efek dan False sebaliknya. Sangat dianjurkan untuk menyetel parameter autoescape ke True, sehingga jika anda memanggil fungsi dari kode Python dia akan mempunyai pelolosan diadakan secara awal.

    Sebagai contoh, mari kita menulis penyaring yang menekankan karakter pertama dari sebuah deretan karakter:

    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)
    

    Bendera needs_autoescape dan argumen kata kunci autoescape berarti bahwa fungsi kami akan mengetahui apakah pelolosan otomatis berpengaruh ketika saringan dipanggil. Kami menggunakan autoescape untuk memutuskan apakah data masukan butuh dilewatkan melalui django.utils.html.conditional_escape atau tidak. (Dalam kasus terakhir, kami menggunakan fungsi penciri sebagai fungsi "escape".) Fungsi conditional_escape() seperti escape() kecuali itu hanya meloloskan masukan yang not instance SafeData. Jika instance SafeData dilewatkan ke conditional_escape(), data dikembalikan tidak berubah.

    Akhirnya, dalam contoh diatas, kami ingat untuk menandai hasil sebagai aman sehingga HTML kami dimasukkan langsung kedalam cetakan tanpa pelolosan lanjut.

    Tidak perlu khawatir tentang bendera is_safe dalam kasus ini (meskipun termasuknya tidak akaan melukai apapun). Kapanpun anda secara manual menangani masalah pelolosan otomatis dan mengembalikan sebuah deretan karakter aman, bendera is_safe tidak merubah apapun.

Peringatan

Menghindari kerentanan XSS ketika menggunakan ulang filter bawaan

Penyaring siap pakai Django mempunyai autoescape=True secara awal agar mendapatkan kebiasaan pelolosan otomatis yang benar dan menghindari kerentanan cross-site script.

Dalam versi lama Django, berhati-hatilah ketika menggunakan penyaring pasang-tetap Django sebagai autoescape nilai awal ke None. Anda akan butuh melewatkan autoescape=True untuk mendapatkan pelolosan otomatis.

Sebagai contoh, jika anda ingin menulis penyesuaian penyaring dipanggil urlize_and_linebreaks yang menggabungkan penyaring urlize dan linebreaksbr, penyaring akan kelihatan seperti:

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)

Kemudian:

{{ comment|urlize_and_linebreaks }}

akan setara dengan:

{{ comment|urlize|linebreaksbr }}

Saringan dan zona waktu

Jika anda menulis sebuah penyesuaian penyaring yang dijalankan pada obyek datetime, anda akan biasanya mendaftarkannya dengan bendera expects_localtime disetel ke True:

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

Ketika bendera ini disetel, jika argumen pertama pada penyaring anda adalah zona waktu perhatikan datetime, Django akan merubahnya ke zona waktu saat ini sebelum melewatkannya ke penyaring anda ketika sesuai, menurut pada aturan untuk perubahan zona waktu dalam cetakan.

Menulis etiket cetakan penyesuaian

Etiket lebih rumit daripada penyaring, karena etiket dapat melakukan apapun. Django menyediakan sejumlah jalan pintas yang membuat penulisan kebanyakan jenis etiket lebih mudah. Pertama kami akan menjelajahi jalan pintas tersebut, kemudian menjelaskan bagaimana menulis sebuah etiket dari goresan untuk kasus-kasus tersebut ketika jalan pintas tidak cukup kuat.

Etiket sederhana

django.template.Library.simple_tag()

Banyak cetakan etiket mengambil sejumlah argumen -- deretan karakter atau cetakan variabel -- dan mengembalikan sebuah hasil setelah melakukan beberapa pengolahan berdasarkan hanya pada masukan argumen dan beberapa informasi luar. Sebagai contoh, sebuah etiket current_time mungkin menerima sebuah bentuk deretan karakter dan mengembalikan waktu sebagai bentuk deretan karakter.

Untuk meringankan pembuatan dari jenis ini dari etiket, Django menyediakan fungsi pembantu, simple_tag. Fungsi ini, yaitu sebuah cara dari django.template.Library, mengambil sebuah fungsi yang menerima angka apapun dari argumen, membungkusnya dalam fungsi render dan bit-bit yang dibutuhkan lainnya disebutkan diatas dan mendaftarkannya dengan cetakan sistem.

Fungsi current_time kami dapat jadi ditulis seperti ini:

import datetime
from django import template

register = template.Library()


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

Beberapa hal untuk dicatat tentang fungsi pembantu simple_tag:

  • Memeriksa angka wajib dari argumen, dll., telah selesai dilakukan oleh waktu fungsi kami dipanggil, jadi kami tidak butuh melakukan itu.
  • Kutipan sekitar argumen (jika ada) telah dihilangkan, jadi kami menerima string polos.
  • Jika argumen adalah cetakan variabel, fungsi kami dilewatkan nilai sast ini dari variabel, bukan variabel itu sendiri.

Tidak seperti peralatan etiket lain, simple_tag melewatkan keluarannya melalui conditional_escape() jika konteks cetakan dalam suasana pelolosan otomatis, untuk memastikan HTML benar dan melindungi anda dari kerentanan XSS.

Jika tambahan pelolosan tidak diharapkan, anda akan butuh menggunakan mark_safe() jika anda sepenuhnya yakin bahwa kode anda tidak mengandung kerentanan XSS. Untuk membangun potongan kecil HTML, gunakan format_html() daripada mark_safe() sangat kuat dianjurkan.

Jika etiket cetakan anda butuh untuk mengakses konteks saat ini, anda dapat menggunakan argumen takes_context ketika mendaftarkan etiket anda:

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

Catat bahwa argumen pertama harus dipanggil context.

Untuk informasi lebih pada bagaimana pilihan takes_context bekerja, lihat bagian pada inclusion tags.

Jika anda butuh menamai kembali etiket anda, anda dapat menyediakan nama penyesuaian untuknya:

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


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

Fungsi simple_tag mungkin menerima angka apapun dari argumen terkait atau kata kunci. Sebagai contoh:

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

Kemudian dalam cetakan angka apapun dari argumen, dipisahkan oleh spasi, mungkin dilewatkan ke cetakan etiket. Seperti dalam Python, nilai untuk argumen katakunci disetel menggunakan tanda sama dengan ("=") dan harus disediakan setelah penempatan argumen. Sebagai contoh:

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

Itu dimungkinkan untuk menyimpan hasil etiket dalam sebuah cetakan variabel daripada secara langsung mengeluarkannya. Ini dilakukan dengan menggunakan argumen as diikuti oleh nama variabel. Melakukannya mengadakan anda untuk mengeluarkan isi diri anda dimana anda melihatnya cocok:

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

Penyertaaan etiket

django.template.Library.inclusion_tag()

Jenis umum lainnya dari cetakan etiket adalah jenis yang menampilkan beberapa data dengan membangun cetakan lain. Sebagai contoh, antarmuka admin Django menggunakan penyesuaian etiket cetakan untuk menampilkan tombol dibawah dari halaman formulir "add/change". Tombol-tombol tersebut selalu terlihat sama, tetapi ssaran tautan berubah tergantung pada obyek yang sedang disunting -- sehingga mereka kasus sempurna untuk menggunakan cetakan kecil yang diisi dengan rincian dari obyek saat ini. (Dalam kasus admin, ini adalah etiket submit_row.)

Urutan etiket ini dipanggil "penyertaan etiket".

Menulis pencatuman etiket adalah mungkin pertunjukan terbaik dengan contoh. Mari kita menulis sebuah etiket yang mengeluarkan daftar pilihan untuk obyek Poll yang diberikan, seperti dibuat dalam tutorial. Kami akan menggunakan etiket seperti ini:

{% show_results poll %}

...dan keluaran akan kelihatan seperti ini:

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

Pertama, tentukan fungsi yang mengambil argumen dan menghasulkan kamus dari data untuk hasil. Titik terpenting disini adalah kami hanya butuh mengembalikan sebuah kamus, bukan apapun lebih rumit. Ini akan digunakan sebagai konteks cetakan untuk fragmen cetakan. Contoh:

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

Selanjutnya, buat cetakan digunakan untuk membangun keluaran etiket. Cetakan ini adalah fitur tetap etiket: penulis etiket menentukannya, bukan perancang cetakan. Mengikuti contoh kami, cetakan sangat pendek:

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

Sekarang, buat dan daftar penyertaan etiket dengan memanggil cara inclusion_tag() pada sebuah obyek Library. Mengikuti contoh kami, jika cetakan diatas dalam sebuah berkas dipanggil results.html dalam sebuah direktori yang dicari oleh pemuat cetakan, kami akan mendaftar etiket seperti ini:

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

Kemungkinan lain dia memungkinkan mendaftarkan pemasukan etiket menggunakan instace django.template.Template:

from django.template.loader import get_template

t = get_template("results.html")
register.inclusion_tag(t)(show_results)

...ketika pertama membuat fungsi.

Terkadang, penyertaan etiket anda mungkin membutuhkan sejumlah besar argumen, membuatnya sakit untuk cetakan penulis melewatkan semua argumen dan mengingat urutan mereka. Untuk mengatasi ini, Django menyediakan sebuah pilihan takes_context untuk penyertaan etiket. Jika anda menentukan takes_context dalam sebuah cetakan etiket, etiket tidak akan mempunyai argumen wajib, dan pokok fungsi Python akan mempunyai satu argumen -- konteks cetakan mulai ketika etiket dipanggil.

Sebagai contoh, katakan anda sedang menulis sebuah penyertaan etiket yang akan selalu digunakan dalam sebuah konteks yang mengandung variabel home_link dan home_title yang menunjuk kembali ke halaman utama. Disini fungsi Python akan kelihatan seperti:

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

Catat bahwa parameter pertama pada fungsi harus dipanggil context.

Dalam baris register.inclusion_tag() itu, kami mencirikan takes_context=True dan nama dari cetakan. Ini adalah cetakan link.html mungkin kelihatan seperti:

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

Lalu, kapanpun anda ingin menggunakan tag tersuai, muat pustakanya dan panggil tanpa argument, seperti:

{% jump_link %}

Catat bahwa ketika anda menggunakan takes_context=True, tidak perlu melewatkan argumen ke etiket cetakan. Dia otomatis mendapatkan akses ke konteks.

Parameter takes_context awalnya adalah False. Ketika disetel ke True, etiket melewatkan obyek konteks, seperti dalam contoh ini. Itu hanya oerbedaan diantara kasus ini dan contoh inclusion_tag sebelumnya.

Fungsi inclusion_tag mungkin menerima angka apapun dari penempatan atau argumen kata kunci. Sebagai contoh:

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

Kemudian dalam cetakan angka apapun dari argumen, dipisahkan oleh spasi, mungkin dilewatkan ke cetakan etiket. Seperti dalam Python, nilai untuk argumen katakunci disetel menggunakan tanda sama dengan ("=") dan harus disediakan setelah penempatan argumen. Sebagai contoh:

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

Etiket cetakan penyesuaian tingkat lanjut

Terkadang fitur dasar untuk pembuatan etiket cetakan penyesuaian tidak cukup. Jangan khawatir, Django memberikan anda akses lengkap ke internal yang diwajibkan untuk membangun sebuah etiket cetakan dari bawah ke atas.

Ikhtisar cepat

Sistem cetakan bekerja dalam dua langkah pengolahan: menyusun dan membangun. Untuk menentukan etiket cetakan penyesuaian, anda menentukan bagaimana penyusunan bekerja dan bagaimana pembangunan bekerja.

Ketika Django menyusun cetakan, itu membagi teks cetakan mentah menjadi ''nodes''. Setiap node adalah instance dari django.template.Node dan memiliki metode render(). Cetakan tersusun adalah daftar obyek Node. Ketika anda memanggil render() di obyek cetakan tersusun, cetakan memanggil render() pada masing-masing Node dalam daftar nodenya, dengan konteks yang diberikan. Hasil adalah semua digabungkan bersama-sama untuk membentuk keluaran cetakan.

Jadi, untuk menentukan penyesuaian cetakan etiket, anda menentukan bagaimana cetakan etiket mentah dirubah kedalam Node (fungsi penyusunan), dan apa cara render() node lakukan.

Menulis fungsi himpunan

Untuk setiap etiket cetakan pertemuan pengurai cetakan, dia memanggil fungsi Python dengan isi etiket dan pengurai obyek itu sendiri. Fungsi ini bertanggung jawab untuk mengembalikan sebuah contoh Node berdasarkan pada isi dari etiket.

Sebagai contoh, mari kita menulis penerapan penuh dari etiket cetakan kami, {% current_time %}, yang menampilkan tanggal/waktu saat ini, dibentuk menurut parameter yang diberikan pada etiket, di sintaks strftime(). Adalah ide bagus untuk memutuskan sintak etiket sebelum yang lainnya. Pada kasus kamu, katakanlah etiket harus digunakan seperti ini:

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

Pengurai untuk fungsi ini harus mengambil parameter dan membuat obyek 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])

Catatan:

  • parser` adalah obyek pengurai cetakan. Kami tidak membutuhkannya dalam contoh ini.
  • token.contents adalah deretan karakter dari isi mentah dari etiket. Dalam contoh kami, dia adalah 'current_time "%Y-%m-%d %I:%M %p"'.
  • Cara token.split_contents() memisahkan argumen pada ruang selama menjaga deretan karakter dikutip bersama. Lebih mudah token.contents.split() tidak akan menjadi kuat, karena akan naif pisah pada semua ruang, termasuk itu dalam deretan karakter dikutip. Adalah ide bagus untuk selalu menggunakan token.split_contents().
  • Fungsi ini bertanggungjawab untuk memunculkan django.template.TemplateSyntaxError, dengan pesan membantu, untuk sintakses kesalahan apapun.
  • Pengecualian TemplateSyntaxError menggunakan variabel tag_name. Jangan mengkode keraskan nama etiket dalam pesan-pesan kesalahan anda, karena itu memasangkan nama etiket ke fungsi anda. token.contents.split()[0] akan ''selalu'' menjadi nama dari etiket anda -- bahkan ketika etiket tidak mempunyai argumen.
  • Fungsi mengembalikan sebuah CurrentTimeNode dengan apapun node butuh diketahui tentang etiket ini. Dalam kasus ini, itu melewatkan argumen -- "%Y-%m-%d %I:%M %p". Kutipan terkemuka dan tertinggal dari etiket cetakan dipindahkan dalam format_string[1:-1].
  • Penguraian adalah tingkat-rendah. Pengembang Django mempunyai percobaan dengan menulis kerangka kecil diatas dari sistem penyurai ini, menggunakan teknik seperti tata bahasa EBNF, tetapi percobaan tersebut membuat cetakan mesin terlalu lambat. Dia adalah tingkat-rendah karena itu tercepat.

Menulis pembangun

Langkah kedua dalam menulis penyesuaian etiket adalah menentukan sebuah subkelas Node yang mempunyai cara render().

Melanjutkan contoh diatas, kami butuh menentukan 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)

Catatan:

  • __init__() mendapatkan format_string dari do_current_time(). Selalu melewati options/parameters/arguments apapun ke Node melalui __init__() nya.
  • Cara render() adalah dimana pekerjaan sebenarnya terjadi.
  • render() harus pada umumnya gagal secara diam, khususnya dalam lingkungan produksi. Dalam beberapa kasus bagaimanapun, khususnya jika context.template.engine.debug adalah True, cara ini mungkin memunculkan sebuah pengecualian untuk membuat mencari kesalahan lebih mudah. Sebagai contoh, beberapa etiket inti memunculkan django.template.TemplateSyntaxError jika mereka menerima nomor atau jenis salah dari argumen

Akhirnya, pemisahan ini dari hasil penyusunan dan pembangunan dalam sebuah sistem cetakan efesien, karena sebuah cetakan dapat membangun banyak konteks tanpa harus diuraikan banyak kali.

Pertimbangan pelolosan otomatis

Keluaran dari cetakan etiket tidak* otomatis berjalan melalui penyaring pelolosan otomatis (dengan pencegualian dari simple_tag() seperti yang digambarkan diatas). Bagaimanapun, masih terdapat sepasang hal anda harus ingat ketika menulis sebuah cetakan etiket.

Jika metode render() dari etiket cetakan anda menyimpan hasil dalam variabel konteks (daripada mengembalikan hasil dalam string), itu harus berhati-hati untuk memanggil mark_safe() jika pantas. Ketika variabel akhirnya dibangun, itu akan dipengaruhi oleh pengaturan pelolosan-otomatis pada saat itu, sehingga isi yang harusnya aman dari pelolosan lebih lanjut butuh ditandai begitu saja.

Juga, jika cetakan etiket anda membuat konteks baru untuk melakukan beberapa sub pembangunan, setel atribut pelolosan otomatis ke nilai konteks saat ini. Cara __init__ untuk kelas Context mengambil parameter dipanggil autoescape dimana anda dapat menggunakan untuk tujuan ini. Sebagai contoh:

from django.template import Context


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

Ini bukan situasi paling umum, tetapi ini berguna jika anda membangun cetakan anda sendiri. Sebagai contoh:

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

Jika kami mempunyai terlantar untuk dilewatkan dalam nilai context.autoescape saat ini ke Context baru kami dalam contoh ini, hasil akan mempunyai selalu otomatis diloloskan, yang mungkin perilaku yang tidak diinginkan jika cetakan etiket digunakan dalam sebuah blok {% autoescape off %}.

Pertimbangan Thread-safety

Sekali node diuraikan, cara render dia mungkin dipanggil sejumlah kali. Sejak Django terkadang berjalan dalam lingkungan banyak-rangkaian, sebuah node tunggal mungkin secara berkelanjutan membangun dengan konteks berbeda dalam menjawab ke dua permintaan terpisah. Oleh sebab itu, sangat penting untuk memastikan cetakan etiket anda adalah aman.

Untuk memastikan cetakan etiket anda thread safe, anda harus jangan menyimpan informasi keadaan pada node itu sendiri. Sebagai contoh, Django menyediakan cetakan etiket cycle siap pakai yang berputar terhadap sebuah daftar dari deretan karakter yang diberikan setiap kali dia dibangun:

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

Sebuah penerapan yang dibuat-buat dari CycleNode mungkin kelihatan kurang lebih seperti ini:

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)

Tetapi, seandainya kami mempunyai dua cetakan membangun potongan cetakan dari atas pada waktu bersamaan:

  1. Urutan 1 melakukan pengulangan pertamanya, CycleNode.render() mengembalikan 'row1'
  2. Urutan 2 melakukan pengulangan pertamanya, CycleNode.render() mengembalikan 'row2'
  3. Urutan 1 melakukan pengulangan keduanya, CycleNode.render() mengembalikan 'row1'
  4. Urutan 2 melakukan pengulangan keduanya, CycleNode.render() mengembalikan 'row2'

CycleNode berulang, tetapi berulang secara global. Sejauh Thread 1 dan Thread 2 terkait, itu selalu mengembalikan nilai yang sama. Ini bukan yang kita inginkan!

Untuk mengalamatkan masalah ini, Django menyediakan sebuah render_context yang terhubung dengan context dari cetakan yang saat ini sedang dibangun. render_context berperilaku seperti sebuah kamus Python, dan harus digunakan untuk menyimpan keadaan Node diantara permohonan dari cara render.

Mari kita refactor penerapan CycleNode kami untuk menggunakan 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)

Catat bahwa ini sangat aman untuk menyimpan informasi umum yang tidak akan merubah keseluruhan hidup dari Node sebagai sebuah atribut. Dalam kasus CycleNode, argumen cyclevars tidak berubah setelah Node dipakai, jadi kami tidak butuh menaruhnya ke dalam render_context. Tetapi keadaan informasi yang khusus pada cetakan yang saat ini sedang dibangun, seperti putaran saat ini dari CycleNode, harus disimpan dalam render_context.

Catatan

Perhatikan bagaimana kami menggunakan self untuk menjangkau CycleNode informasi tertentu dalam render_context. Disana mungkin banyak CycleNodes dalam cetakan yang diberikan, jadi kami butuh berhati-hati tidak megnkritik informasi keadaan node lainnya. Cara termudah melakukan ini adalah selalu menggunakan self sebagai kunci kedalam render_context. Jika anda sedang menjaga jalur dari beberapa variabel keadaan, buat render_context[self] sebuah kamus.

Mendaftarkan etiket

Akhirnya, daftar etiket dengan instance Library modul anda, seperti dijelaskan dalam writing custom template tags diatas. Contoh:

register.tag("current_time", do_current_time)

Cara tag() mengambil dua argumen:

  1. Nama dari cetakan etiket -- sebuah deretan karakter. Jika ini dihilangkan dari fungsi penyusunan akan digunakan.
  2. Fungsi himpunan -- Sebuah fungsi Python (bukan nama fungsi sebagai deretan karakter).

Dengan pendaftaran penyaring, dia juga memungkinkan menggunakan ini sebagai decorator:

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


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

Jika anda meninggalkan argumen name, seperti dalam contoh kedud diatas, Django akan menggunakan nama fungsi sebagai nama etiket.

Melewatkan variabel cetakan ke etiket

Meskipun anda dapat melewatkan angka apapun dari argumen ke etiket cetakan menggunakan token.split_contents(), argumen semua tidak dipaketkan sebagai harfiah string. Sedikit kerja lebih diwajibkan untuk melewatkan isi dinamis (variabel cetakan) ke etiket cetakan sebagai sebuah argumen.

Selagi contoh sebelumnya telah membentuk waktu saat ini ke dalam string dan mengembalikan string, misalkan anda ingin melewatkan di DateTimeField dari sebuah obyek dan mempunyai bentuk etiket cetakan yang tanggal-waktu:

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

Mulanya, token.split_contents() akan mengembalikan tiga nilai:

  1. Nama etiket format_time.
  2. String 'blog_entry.date_updated' (tanpa dikelilingi kutip).
  3. Pembentukan deretan karakter '"%Y-%m-%d %I:%M %p"'. Mengembalikan nilai dari split_contents() akan menyertakan kutipan awal dan akhir untuk deretan karakter persis seperti ini.

Sekarang etiket anda harus kelihatan seperti ini:

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

Anda juga mempunyai kesemparan pembangun untuk mengambil isi sebenarnya dari sifat date_updated dari obyek blog_entry. Ini dapat diselesaikan dengan menggunakan kelas Variable() dalam django.template.

Untuk menggunakan kelas Variable, instansiasikan itu dengan nama dari variabel untuk diselesaikan, dan panggil variable.resolve(context). Jadi, sebagai contoh:

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

Keputusan variabel akan melempar sebuah pengecualian VariableDoesNotExist jika dia tidak dapat menyelesaikan deretan karakter dilewatkan ke dia dalam konteks saat ini dari halaman.

Menyetel sebuah variabel di konteks

Contoh diatas mengengeluarkan nilai. Umumnya, itu lebih fleksibel jika etiket cetakan anda mensetel variabel cetakan daripada mengeluarkan nilai. Cara itu, penulis cetakan dapat menggunakan kembali nilai yang etiket cetakan anda buat.

Untuk mensetel variabel dalam konteks, gunakan penugasan dictionary pada obyek konteks di metode render(). Ini adalah versi terperbaharui dari CurrentTimeNode yang menyetel variabel cetakan current_time daripada mengeluarkannya:

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

Catat bahwa render() mengembalikan deretan karakter kosong. render() harus selalu mengembalikan keluaran deretan karakter. Jika semua cetakan etiket disetel variabel, ``render() harus mengembalikan deretan karakter kosong.

Ini adalah bagaimana anda akan menggunakan versi baru dari etiket:

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

Lingkup variabel di konteks

Apapun kumpulan variabel dalam konteks akan hanya tersedua dalam block yang sama dari cetakan dimana dia ditetapkan. Perilaku ini adalah disengaja; dia menyediakan cakupan untuk variabel sehingga mereka tidak bertentangan dengan konteks di blok lainnya.

Tetapi, ada masalah dengan CurrentTimeNode2: Nama variabel current_time adalah dikode keraskan. Ini berarti anda akan butuh untuk memastikan cetakan anda tidak menggunakan {{ current_time }} dimanapun juga, karena {% current_time %} akan membabi buta meniban nilai variabel itu. Sebuah solusi pembersih adalah membuat cetakan etiket menentukan nama dari variabel keluaran, seperti begitu:

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

Untuk melakukannya, anda akan butuh me refaktor kedua fungsi penyusun dan kelas Node, seperti begitu:

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)

Perbedaan disini adalah bahwa do_current_time() mengambil bentuk deretan karakter dan nama variabel, melewatkan keduanya ke CurrentTimeNode3.

Akhirnya, jika anda hanya butuh mempunyai sintaksis sederhana untuk penyesuaian pembaharuan-konteks cetakan etiket, pertimbangkan menggunakan jalan pintas simple_tag(), yang mendukung menetapkan hasil etiket ke cetakan variabel.

Menguraikan sampai blok etiket lain

Cetakan etiket dapat bekerja dalam berduaan. Sebagai contoh, standar etiket {% comment %} menyembunyikan semua sampai {% endcomment %}. Untuk membuat sebuah cetakan etiket seperti ini, gunakan parser.parse() dalam fungsi penyusun anda

Ini adalah bagaimana menyederhanakan etiket {% comment %} mungkin diterapkan:

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


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

Catatan

Penerapan sebenarnya dari {% comment %} adalah sedikit berbeda dalam memungkinkan merusak cetakan etiket untuk muncul diantara {% comment %} dan {% endcomment %}. Dia melakukannya dengan memanggil parser.skip_past('endcomment') daripada parser.parse(('endcomment',)) diikuti oleh parser.delete_first_token(), jadi menghindari generasi dari sebuah daftar node.

parser.parse() mengambil sebuah tuple dari nama dari blok etiket "untuk diurai sampai". Dia mengembalikan sebuah contoh dari django.template.NodeList, yaitu sebuah daftar dari semua obyek Node yang pengurai jumpai "sebelum" dia menjumpai etiket dinamai apapun dalam tuple.

Dalam "nodelist = parser.parse(('endcomment',))" di contoh atas, nodelist adalah sebuah daftar dari semua node diantara {% comment %} dan {% endcomment %}, bukan menghitung {% comment %} dan {% endcomment %} mereka sendiri.

Setelah parser.parse() dipanggil, pengurai belum "mengkonsumsi" etiket {% endcomment %}, jadi kode butuh secara eksplisit memanggil parser.delete_first_token().

CommentNode.render() mengembalikan string kosong. Apapun diantara {% comment %} dan {% endcomment %} diabaikan.

Mengurai sampai blok etiket lainnya, dan menyimpan isi

Di contoh sebelumnya, do_comment() menyingkirkan apapun diantara {% comment %} and {% endcomment %}. Daripada melakukan itu, dimungkinkan untuk melakukan sesuatu dengan kode diantara blok etiket.

Sebagai contoh, ini adalah penyesuaian cetakan etiket, {% upper %}, yang menghuruf besarkan semuanya diantara dia sendiri dan {% endupper %}.

Penggunaan:

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

Seperti di contoh sebelumnya, kami akan menggunakan parser.parse(). Tetapi kali ini, kami melewatkan hasil nodelist ke 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()

Satu-satunya konsep disini adalah self.nodelist.render(context) dalam UpperNode.render().

For more examples of complex rendering, see the source code of {% for %} in django/template/defaulttags.py and {% if %} in django/template/smartif.py.

Back to Top