مقدمه: چرا قالب‌های جنگو؟

Django Template Language (DTL) یا زبان قالب جنگو، یکی از بنیادی‌ترین اجزای فریم‌ورک جنگو است که با فلسفه‌ای متفاوت از بسیاری از زبان‌های قالب‌بندی دیگر طراحی شده است. برخلاف Jinja2 یا Smarty که امکان تزریق مستقیم کد پایتون را فراهم می‌کنند، DTL عمداً محدود طراحی شده تا منطق نمایش (Presentation Logic) را از منطق برنامه (Program Logic) جدا نگه دارد.

این جداسازی، یک تصمیم معماری آگاهانه است. اگر پیش‌زمینه برنامه‌نویسی دارید، ممکن است در ابتدا محدودکننده به‌نظر برسد، اما در عمل، همین محدودیت باعث می‌شود قالب‌ها خواناتر، قابل‌نگهداری‌تر و امن‌تر باقی بمانند. توسعه‌دهنده فرانت‌اند می‌تواند بدون نیاز به درک عمیق از منطق پایتون، روی طراحی تمرکز کند و توسعه‌دهنده بک‌اند می‌تواند بدون نگرانی از دستکاری منطق در قالب، کد خود را بنویسد.

در این راهنما، از ساده‌ترین مفاهیم مانند متغیرها و تگ‌ها شروع می‌کنیم و به‌تدریج به مباحث پیچیده‌تر مانند تگ‌های سفارشی، Context Processorها، بهینه‌سازی عملکرد و امنیت می‌رسیم.

۱. متغیرها در قالب‌های جنگو

۱.۱. سینتکس پایه

متغیرها در قالب جنگو با {{ }} (دابل کِرلی بریس) نمایش داده می‌شوند. وقتی موتور قالب به یک متغیر برخورد می‌کند، آن را ارزیابی کرده و مقدارش را جایگزین می‌کند:

{{ variable }}

نام متغیر می‌تواند شامل حروف، اعداد و آندرلاین (_) باشد، اما نمی‌تواند با آندرلاین شروع شود یا صرفاً عدد باشد. همچنین، فاصله و کاراکترهای نگارشی در نام متغیر مجاز نیستند.

۱.۲. دسترسی به Attributeها با نقطه

یکی از ویژگی‌های مهم DTL، استفاده از نقطه (.) برای دسترسی به Attributeها یا Methodهاست. وقتی موتور قالب یک نقطه می‌بیند، به ترتیب زیر جستجو می‌کند:

  1. Dictionary Lookup: جستجو به‌عنوان کلید در دیکشنری
  2. Attribute/Method Lookup: جستجو به‌عنوان Attribute یا Method روی شیء
  3. Numeric Index Lookup: جستجو به‌عنوان ایندکس عددی در لیست
{{ user.name }}
{{ user.get_full_name }}
{{ items.0 }}

نکته حرفه‌ای: این ترتیب جستجو به این معناست که اگر یک دیکشنری و یک Attribute با نام یکسان داشته باشید، دیکشنری اولویت دارد. این رفتار می‌تواند در پروژه‌های بزرگ به منبع باگ‌های ظریف تبدیل شود. توصیه می‌کنم در زمان طراحی Context، از نام‌های مشخص و غیرمشترک استفاده کنید.

۱.۳. رفتار با متغیرهای نامعتبر

اگر متغیری در Context وجود نداشته باشد، جنگو به‌طور پیش‌فرض string_if_invalid را نمایش می‌دهد که به‌صورت پیش‌فرض یک رشته خالی است. این رفتار، برخلاف Jinja2 که ممکن است خطا ایجاد کند، انعطاف‌پذیری بیشتری برای قالب‌های تولیدشده به‌صورت داینامیک فراهم می‌کند.

می‌توانید این رفتار را در تنظیمات TEMPLATES تغییر دهید:

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "OPTIONS": {
            "string_if_invalid": "[[INVALID: %s]]",
        },
    },
]

هشدار حرفه‌ای: استفاده از string_if_invalid با یک رشته غیرخالی در محیط Production توصیه نمی‌شود، زیرا ممکن است اطلاعات حساس یا نام‌های متغیرهای داخلی را افشا کند. این تنظیم را فقط در محیط توسعه فعال کنید.

۲. فیلترها (Filters)

فیلترها ابزارهایی برای تغییر نحوه نمایش داده‌ها هستند. جنگو حدود شصت فیلتر داخلی ارائه می‌دهد که بسیاری از نیازهای رایج را پوشش می‌دهند.

۲.۱. سینتکس پایه

{{ variable|filter_name }}
{{ variable|filter_name:"argument" }}

نشان | (پایپ) متغیر را از فیلتر جدا می‌کند. فیلترها می‌توانند زنجیره‌ای شوند:

{{ name|lower|capfirst }}

۲.۲. فیلترهای ضروری و پرکاربرد

فیلترکاربردمثال
capfirstبزرگ کردن حرف اول{{ "hello"|capfirst }} → Hello
lowerتبدیل به حروف کوچک{{ "HELLO"|lower }} → hello
upperتبدیل به حروف بزرگ{{ "hello"|upper }} → HELLO
lengthبرگرداندن طول{{ items|length }}
defaultمقدار پیش‌فرض در صورت خالی بودن{{ name|default:"ناشناس" }}
truncatewordsکوتاه کردن با تعداد کلمه{{ text|truncatewords:"100" }}
dateفرمت تاریخ{{ post.date|date:"Y-m-d" }}
floatformatفرمت اعشاری{{ price|floatformat:2 }}
safeعلامت‌گذاری به‌عنوان HTML امن{{ html_content|safe }}
escapeفرار دادن HTML{{ user_input|escape }}
urlizeتبدیل URL به لینک{{ text|urlize }}
linebreaksتبدیل خطوط جدید به p{{ text|linebreaks }}

۲.۳. فیلترهای تاریخ و زمان

جنگو از فیلتر date با فرمت‌بندی مشابه PHP پشتیبانی می‌کند:

{{ post.published_at|date:"j F Y" }}
{{ post.published_at|date:"l, j F Y - H:i" }}
{{ post.published_at|timesince }}
{{ post.published_at|timeuntil }}

فیلتر timesince و timeuntil بسیار کاربردی هستند و به‌صورت خودکار بازه زمانی نسبی را محاسبه می‌کنند.

۲.۴. فیلترهای عددی و آماری

{{ price|floatformat:2 }}          {# 1250000.00 #}
{{ price|floatformat:"-2" }}        {# 1250000 #}
{{ price|intcomma }}                 {# 1,250,000 #}
{{ ratio|floatformat:1 }}           {# 0.7 #}

برای intcomma، باید کتابخانه django.contrib.humanize را در INSTALLED_APPS اضافه کرده و در قالب {% load humanize %} کنید.

نکته حرفه‌ای: فیلتر floatformat با آرگومان منفی (-2)، اعشار را حذف می‌کند. این رفتار در پروژه‌های مالی که نمایش اعشار بی‌معناست، بسیار مفید است.

۲.۵. فیلترهای شرطی

{{ value|default_if_none:"N/A" }}
{{ value|yesno:"بله,خیر,نامشخص" }}
{{ items|first }}
{{ items|last }}
{{ items|random }}
{{ items|slice:":5" }}

فیلتر yesno یک الگوی قدرتمند است که می‌تواند سه حالت (True/False/None) را پوشش دهد.

۳. تگ‌ها (Tags)

تگ‌ها منطق و جریان کنترل را در قالب فراهم می‌کنند. برخلاف فیلترها که فقط داده‌ها را تغییر می‌دهند، تگ‌ها می‌توانند ساختار قالب را تغییر دهند.

۳.۱. تگ if

{% if user.is_authenticated %}
    <p>خوش آمدید، {{ user.username }}</p>
{% elif user.is_staff %}
    <p>پنل مدیریت</p>
{% else %}
    <p>لطفاً وارد شوید.</p>
{% endif %}

عملگرهای مقایسه‌ای: ==, !=, <, >, <=, >=, in, not in, is, is not.

عملگرهای منطقی: and, or, not.

{% if user.age >= 18 and user.country == "IR" %}

نکته حرفه‌ای: در DTL، پرانتز برای گروه‌بندی شرط‌ها وجود ندارد. اگر به منطق پیچیده‌تر نیاز دارید، بهتر است آن را در View یا یک تگ سفارشی محاسبه کنید.

۳.۲. تگ for

{% for item in items %}
    <li>{{ item.name }}</li>
{% empty %}
    <li>هیچ آیتمی وجود ندارد.</li>
{% endfor %}

متغیر forloop اطلاعاتی درباره حلقه فعلی فراهم می‌کند:

متغیرتوضیح
forloop.counterشمارنده از ۱
forloop.counter0شمارنده از ۰
forloop.revcounterشمارنده معکوس از آخر
forloop.firstآیا اولین تکرار است
forloop.lastآیا آخرین تکرار است
forloop.parentloopحلقه والد در حلقه‌های تودرتو

هشدار عملکردی: استفاده از حلقه‌های تودرتو در قالب می‌تواند به مشکلات N+1 Query منجر شود. اگر در حلقه به یک Relation دسترسی دارید (مانند {{ post.author.name }})، Django برای هر تکرار یک Query جداگانه اجرا می‌کند. همیشه از select_related و prefetch_related در View استفاده کنید.

۳.۳. تگ with

تگ with برای ذخیره یک مقدار در یک متغیر محلی در قالب استفاده می‌شود:

{% with total=cart.items.count %}
    <p>تعداد آیتم‌ها: {{ total }}</p>
{% endwith %}

{% with user.get_full_name as full_name %}
    <h1>{{ full_name }}</h1>
{% endwith %}

نکته حرفه‌ای: تگ with زمانی مفید است که یک متد یا فیلتر پرهزینه را چندین بار در یک بخش استفاده می‌کنید. با ذخیره نتیجه در یک متغیر، از محاسبه مکرر جلوگیری می‌کنید.

۳.۴. تگ url

{% url "app_name:view_name" arg1 arg2 %}
<a href="{% url "blog:post_detail" post.id %}">{{ post.title }}</a>
<a href="{% url "user_profile" username=user.username %}">پروفایل</a>

استفاده از تگ url به‌جای URLهای هاردکد شده، یک رویه استاندارد است. اگر ساختار URLها تغییر کند، نیازی به ویرایش قالب‌ها نیست.

۳.۵. تگ include

تگ include یک قالب دیگر را در محل فعلی رندر می‌کند:

{% include "partials/header.html" %}
{% include "partials/sidebar.html" with categories=categories %}
{% include "partials/user_card.html" with user=user only %}

تفاوت include و extends: تگ include یک قالب دیگر را درون قالب فعلی قرار می‌دهد (ترکیب). تگ extends یک قالب والد را ارث‌بری می‌کند (وراثت).

نکته حرفه‌ای: استفاده از include با آرگومان only باعث می‌شود Context فعلی به قالب درون‌ریزی‌شده منتقل نشود. این کار، عملکرد را بهبود می‌دهد و از تداخل نام متغیرها جلوگیری می‌کند.

۳.۶. تگ load

{% load static %}
{% load humanize %}
{% load i18n %}
{% load cache %}

۳.۷. تگ csrf_token

<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">ارسال</button>
</form>

تگ csrf_token یک توکن مخفی برای محافظت در برابر حملات Cross-Site Request Forgery (CSRF) تولید می‌کند. این تگ باید در تمام فرم‌های POST استفاده شود.

۳.۸. تگ ifchanged

{% for item in items %}
    {% ifchanged item.category %}
        <h2>{{ item.category }}</h2>
    {% endifchanged %}
    <p>{{ item.name }}</p>
{% endfor %}

این تگ برای گروه‌بندی بصری آیتم‌ها بر اساس یک فیلد بسیار مفید است.

۳.۹. تگ cycle

{% for item in items %}
    <li class="{% cycle "odd" "even" %}">{{ item.name }}</li>
{% endfor %}

{% for item in items %}
    <li class="{% cycle "odd" "even" as row_class %}">{{ row_class }}</li>
{% endfor %}

۳.۱۰. تگ spaceless

{% spaceless %}
    <div>
        <p>متن</p>
    </div>
{% endspaceless %}

نکته: این تگ در نسخه‌های جدیدتر Django توصیه نمی‌شود و استفاده از ابزارهای Minify خارجی ترجیح داده می‌شود.

۴. ارث‌بری قالب (Template Inheritance)

ارث‌بری قالب، یکی از قدرتمندترین ویژگی‌های DTL است که امکان طراحی قالب‌های قابل‌استفاده مجدد را فراهم می‌کند.

۴.۱. ساختار پایه

قالب والد (base.html):

<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}سایت من{% endblock %}</title>
    {% load static %}
    <link rel="stylesheet" href="{% static "css/main.css" %}">
</head>
<body>
    <header>
        {% include "partials/header.html" %}
    </header>
    <main>
        {% block content %}{% endblock %}
    </main>
    <footer>
        {% include "partials/footer.html" %}
    </footer>
</body>
</html>

قالب فرزند:

{% extends "base.html" %}
{% load static %}

{% block title %}درباره ما{% endblock %}

{% block content %}
    <h1>درباره ما</h1>
    <p>متن درباره ما</p>
{% endblock %}

۴.۲. قوانین کلیدی

  1. تگ extends باید اولین خط قالب باشد. اگر قبل از آن محتوایی وجود داشته باشد، نادیده گرفته می‌شود و ممکن است باعث خطاهای غیرمنتظره شود.
  2. محتوای خارج از بلوک‌ها در قالب فرزند نادیده گرفته می‌شود. این یکی از منابع رایج اشتباه است.
  3. می‌توان از بلوک‌های تودرتو استفاده کرد، اما معمولاً نشانه‌ای از طراحی پیچیده است.

۴.۳. ارث‌بری چندسطحی

می‌توان زنجیره‌ای از ارث‌بری ایجاد کرد:

base.html → layouts/dashboard.html → pages/dashboard_index.html
{# layouts/dashboard.html #}
{% extends "base.html" %}
{% block content %}
    <div class="dashboard-layout">
        {% block dashboard_content %}{% endblock %}
    </div>
{% endblock %}

نکته حرفه‌ای: در پروژه‌های بزرگ، معمولاً یک ساختار سه‌سطحی ایجاد می‌شود: base.html (ساختار HTML)، base_public.html و base_dashboard.html (ساختارهای خاص)، و صفحات نهایی.

۴.۴. تگ block با محتوای پیش‌فرض

{% block sidebar %}
    <aside>سایدبار پیش‌فرض</aside>
{% endblock %}

۴.۵. متغیر block.super

{% block content %}
    {{ block.super }}
    <p>محتوای اضافه</p>
{% endblock %}

block.super محتوای بلوک والد را برمی‌گرداند. این ویژگی برای افزودن محتوا به بلوک والد بدون از دست دادن محتوای اصلی بسیار مفید است.

۵. تگ‌ها و فیلترهای سفارشی

زمانی که تگ‌ها و فیلترهای داخلی کافی نباشند، می‌توانید تگ‌ها و فیلترهای سفارشی با پایتون تعریف کنید.

۵.۱. ساختار پوشه‌ها

myapp/
    __init__.py
    models.py
    views.py
    templatetags/
        __init__.py
        custom_tags.py

سپس در قالب:

{% load custom_tags %}

نکته امنیتی: تنها اپلیکیشن‌هایی که در INSTALLED_APPS ثبت شده‌اند، می‌توانند تگ‌های سفارشی خود را بارگذاری کنند.

۵.۲. فیلتر سفارشی

from django import template

register = template.Library()

@register.filter
def currency(value):
    """نمایش قیمت با جداکننده هزارگان و واحد تومان"""
    try:
        return f"{int(value):,} تومان"
    except (ValueError, TypeError):
        return value
{% load custom_tags %}
{{ product.price|currency }}

۵.۳. تگ سفارشی ساده (Simple Tag)

@register.simple_tag
def total_price(quantity, unit_price):
    return quantity * unit_price
{% total_price item.quantity item.price %}

۵.۴. تگ Inclusion

@register.inclusion_tag("partials/user_card.html")
def user_card(user):
    return {
        "username": user.username,
        "full_name": user.get_full_name(),
        "avatar": user.profile.avatar.url if hasattr(user, "profile") else None,
    }
{% user_card author %}

۵.۵. تگ Assignment

@register.simple_tag
def get_recent_posts(count=5):
    return Post.objects.order_by("-published_at")[:count]
{% get_recent_posts 10 as recent_posts %}
{% for post in recent_posts %}
    <li>{{ post.title }}</li>
{% endfor %}

توجه: در نسخه‌های جدیدتر Django، assignment_tag حذف شده و simple_tag قابلیت Assignment را نیز پشتیبانی می‌کند.

۵.۶. تگ با دسترسی به Context

@register.simple_tag(takes_context=True)
def query_string(context, **kwargs):
    request = context["request"]
    params = request.GET.copy()
    for key, value in kwargs.items():
        if value is None:
            params.pop(key, None)
        else:
            params[key] = value
    return params.urlencode()
<a href="?{% query_string page=page_obj.next_page_number %}">بعدی</a>

نکته حرفه‌ای: استفاده از takes_context=True یک الگوی قدرتمند است که تگ شما را قادر می‌سازد به request و سایر متغیرهای Context دسترسی داشته باشد.

۶. Context Processorها

Context Processorها توابعی هستند که در هر درخواست، داده‌هایی به Context قالب اضافه می‌کنند.

۶.۱. Context Processor سفارشی

# myapp/context_processors.py

def site_settings(request):
    return {
        "SITE_NAME": "سایت من",
        "SITE_DESCRIPTION": "توضیح سایت",
        "CURRENT_YEAR": datetime.now().year,
    }

۶.۲. ثبت در تنظیمات

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
                "myapp.context_processors.site_settings",
            ],
        },
    },
]

۶.۳. Context Processorهای داخلی جنگو

Context Processorمتغیرهای اضافه‌شده
django.template.context_processors.debugdebug, sql_queries
django.template.context_processors.requestrequest
django.contrib.auth.context_processors.authuser, perms
django.contrib.messages.context_processors.messagesmessages
django.template.context_processors.i18nLANGUAGES, LANGUAGE_CODE
django.template.context_processors.staticSTATIC_URL

هشدار عملکردی: Context Processorها در هر درخواست اجرا می‌شوند. اگر یک Context Processor سنگین داشته باشید، بر عملکرد تمام صفحات سایت اثر می‌گذارد. همیشه داده‌های سبک و کش‌شده را در Context Processor قرار دهید.

۷. بین‌المللی‌سازی (i18n) در قالب‌ها

۷.۱. تگ trans

{% load i18n %}
<h1>{% trans "Welcome to our site" %}</h1>

۷.۲. تگ blocktrans

{% blocktrans with user_name=user.username %}
    Hello, {{ user_name }}! Welcome back.
{% endblocktrans %}

۷.۳. فیلتر trans

{{ message|trans }}

۷.۴. تولید فایل‌های ترجمه

django-admin makemessages -l fa
django-admin compilemessages

نکته حرفه‌ای: اگر پروژه شما چندزبانه نیست، حتماً USE_I18N = False را در تنظیمات قرار دهید. این کار، سربار بین‌المللی‌سازی را حذف می‌کند.

۸. امنیت در قالب‌های جنگو

۸.۱. Escape خودکار HTML

جنگو به‌طور پیش‌فرض تمام متغیرها را Escape می‌کند تا از حملات XSS (Cross-Site Scripting) جلوگیری کند.

{{ user_input }}           {# Escape خودکار #}
{{ html_content|safe }}    {# غیرفعال کردن Escape - خطرناک #}

۸.۲. تگ autoescape

{% autoescape off %}
    {{ html_content }}
{% endautoescape %}

هشدار امنیتی: استفاده از autoescape off یا فیلتر safe روی داده‌های کاربر، خطر جدی XSS ایجاد می‌کند. تنها روی محتوایی که خودتان تولید کرده‌اید و کاملاً قابل‌اعتماد است، از این ویژگی استفاده کنید.

۸.۳. محدودیت‌های Escape خودکار

Escape خودکار جنگو تمام حملات XSS را مسدود نمی‌کند. به‌عنوان مثال:

<style class={{ var }}>...</style>

اگر var مقدار class1 onmouseover=javascript:func() داشته باشد، ممکن است XSS رخ دهد. همیشه مقادیر Attributeها را در کوتیشن قرار دهید.

۹. بهینه‌سازی عملکرد قالب‌ها

۹.۱. کاهش منطق در قالب

منطق پیچیده را در View یا Model قرار دهید، نه در قالب.

۹.۲. استفاده از select_related و prefetch_related

مشکل N+1 Query یکی از رایج‌ترین مشکلات عملکردی است. اگر در قالب به یک Relation دسترسی دارید، حتماً از این متدها استفاده کنید:

posts = Post.objects.select_related("author", "category").prefetch_related("tags")

۹.۳. کش کردن قطعه قالب (Fragment Cache)

{% load cache %}
{% cache 500 sidebar request.user.username %}
    <aside>
        {# محتوای پرهزینه #}
    </aside>
{% endcache %}

نکته حرفه‌ای: کش کردن قطعه قالب برای بخش‌هایی که به‌ندرت تغییر می‌کنند بسیار مؤثر است. اما هرگز بخش‌های شخصی‌سازی‌شده را کش نکنید، مگر اینکه کلید کش را بر اساس user.id تنظیم کنید.

۹.۴. کاهش تعداد include

هر include یک سربار اضافی دارد. اگر یک قالب کوچک را در یک حلقه include می‌کنید، بهتر است محتوای آن را مستقیماً در قالب اصلی قرار دهید یا از inclusion_tag استفاده کنید.

۹.۵. استفاده از with برای محاسبات مکرر

{% with total=cart.get_total_price %}
    <p>{{ total }}</p>
    <p>{{ total|floatformat:2 }}</p>
    <p>{{ total|intcomma }}</p>
{% endwith %}

بدون with، متد get_total_price سه بار فراخوانی می‌شود.

۱۰. الگوهای پیشرفته در قالب‌های جنگو

۱۰.۱. قالب‌های داینامیک بر اساس نوع کاربر

{% if user.is_authenticated %}
    {% include "partials/logged_in_header.html" %}
{% else %}
    {% include "partials/guest_header.html" %}
{% endif %}

۱۰.۲. قالب‌های چندزبانه با hreflang

{% load i18n %}
{% get_available_languages as LANGUAGES %}
{% for lang_code, lang_name in LANGUAGES %}
    <link rel="alternate" hreflang="{{ lang_code }}" href="{{ request.get_full_path }}">
{% endfor %}

۱۰.۳. ساختاردهی داده‌های JSON-LD در قالب

<script type="application/ld+json">
{
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": "{{ post.title|escapejs }}",
    "author": {
        "@type": "Person",
        "name": "{{ post.author.get_full_name|escapejs }}"
    },
    "datePublished": "{{ post.published_at|date:"c" }}"
}
</script>

نکته حرفه‌ای: استفاده از escapejs برای مقادیر داخل JavaScript ضروری است.

۱۰.۴. Pagination حرفه‌ای

{% if page_obj.has_other_pages %}
<nav>
    {% if page_obj.has_previous %}
        <a href="?page={{ page_obj.previous_page_number }}">قبلی</a>
    {% endif %}

    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
            <span class="current">{{ num }}</span>
        {% elif num > page_obj.number|add:"-3" and num < page_obj.number|add:"3" %}
            <a href="?page={{ num }}">{{ num }}</a>
        {% endif %}
    {% endfor %}

    {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">بعدی</a>
    {% endif %}
</nav>
{% endif %}

۱۱. تست قالب‌ها

۱۱.۱. استفاده از assertTemplateUsed

from django.test import TestCase

class BlogViewTest(TestCase):
    def test_post_detail_uses_correct_template(self):
        response = self.client.get("/blog/1/")
        self.assertTemplateUsed(response, "blog/post_detail.html")
        self.assertTemplateUsed(response, "base.html")

۱۱.۲. تست محتوای قالب

def test_post_title_in_template(self):
    response = self.client.get("/blog/1/")
    self.assertContains(response, "عنوان پست")

۱۲. اشتباهات رایج و چگونه از آن‌ها اجتناب کنیم

۱۲.۱. فراموش کردن load

{# خطا: static بارگذاری نشده #}
<link rel="stylesheet" href="{% static "css/main.css" %}">

راه‌حل: افزودن {% load static %} در ابتدای قالب.

۱۲.۲. استفاده از extends در جای اشتباه

تگ extends باید اولین خط قالب باشد.

۱۲.۳. نبود بلوک در قالب فرزند

تمام محتوای قالب فرزند باید داخل بلوک‌ها باشد.

۱۲.۴. استفاده از safe روی داده کاربر

فیلتر safe روی داده‌های کاربر، خطر XSS ایجاد می‌کند.

۱۲.۵. فراموش کردن csrf_token

عدم استفاده از {% csrf_token %} در فرم‌های POST، باعث خطای 403 می‌شود.

۱۲.۶. N+1 Query در حلقه

دسترسی به Relation در حلقه بدون select_related، باعث اجرای Query اضافی برای هر تکرار می‌شود.

۱۲.۷. استفاده از متغیرهای هاردکد شده

استفاده از URLهای هاردکد به‌جای {% url %}، شکننده است.

۱۳. بهترین شیوه‌ها در طراحی قالب‌های حرفه‌ای

۱۳.۱. ساختار پوشه‌ها

templates/
    base.html
    partials/
        header.html
        footer.html
        sidebar.html
    blog/
        base_blog.html
        post_list.html
        post_detail.html
    shop/
        base_shop.html
        product_list.html
        product_detail.html

۱۳.۲. نام‌گذاری

  • از نام‌های توصیفی استفاده کنید: post_detail.html نه detail.html
  • از پیشوند برای گروه‌بندی استفاده کنید: shop_product_list.html
  • از حروف کوچک و آندرلاین استفاده کنید: user_profile.html

۱۳.۳. مستندسازی

{# blog/post_detail.html #}
{# نمایش جزئیات یک پست وبلاگ #}
{# Context: post, related_posts, comments #}

۱۳.۴. DRY (Don't Repeat Yourself)

از include و extends برای جلوگیری از تکرار استفاده کنید.

۱۳.۵. دسترس‌پذیری

<nav aria-label="ناوبری اصلی">
    <ul>
        {% for item in menu_items %}
            <li><a href="{{ item.url }}" {% if item.active %}aria-current="page"{% endif %}>{{ item.title }}</a></li>
        {% endfor %}
    </ul>
</nav>

۱۴. مباحث پیشرفته: تگ‌های سطح پایین

۱۴.۱. ساختار یک تگ از صفر

from django import template
from django.template.base import Node

class RecentPostsNode(Node):
    def __init__(self, count):
        self.count = int(count)

    def render(self, context):
        from myapp.models import Post
        posts = Post.objects.order_by("-published_at")[:self.count]
        context["recent_posts"] = posts
        return ""

@register.tag
def recent_posts(parser, token):
    try:
        tag_name, count = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError(
            "%r tag requires exactly one argument" % token.contents.split()[0]
        )
    return RecentPostsNode(count)

۱۴.۲. کامپایل و رندر دو مرحله‌ای

سیستم قالب جنگو در دو مرحله کار می‌کند:

  1. Compilation: تجزیه و تبدیل قالب به یک درخت Node
  2. Rendering: اجرای Nodeها با Context

درک این دو مرحله، برای نوشتن تگ‌های بهینه ضروری است.

۱۵. مقایسه با Jinja2

ویژگیDjango TemplateJinja2
سینتکسمشابهمشابه
منطق در قالبمحدودانعطاف‌پذیر
ExtensionCustom TagsMacros, Extensions
عملکردخوبسریع‌تر
امنیتEscape خودکار قوینیاز به تنظیم
یادگیریساده‌ترپیچیده‌تر

اگر نیاز به منطق پیچیده در قالب دارید، Jinja2 گزینه بهتری است. اما اگر می‌خواهید منطق را از قالب جدا نگه دارید، DTL انتخاب درستی است.

نتیجه‌گیری

قالب‌های جنگو یکی از قوی‌ترین ابزارهای فریم‌ورک Django برای جداسازی منطق برنامه از نمایش هستند. از ساده‌ترین متغیرها و فیلترها تا تگ‌های سفارشی و Context Processorهای پیشرفته، DTL مجموعه‌ای جامع از ابزارها را برای ساخت رابط‌های کاربری پویا فراهم می‌کند.

تسلط بر قالب‌های جنگو نه‌تنها به معنای نوشتن کدهای بهتر است، بلکه به معنای درک عمیق‌تر از فلسفه طراحی Django و جداسازی Concerns است. با رعایت اصولی که در این راهنما بررسی شد — از ارث‌بری هوشمندانه و تگ‌های سفارشی گرفته تا امنیت و بهینه‌سازی عملکرد — می‌توانید قالب‌هایی طراحی کنید که هم زیبا، هم سریع و هم قابل‌نگهداری باشند.

اگر این موضوع را در پروژه‌ای پیاده کردید، برایم جالب است بدانم کدام بخش از کار با قالب‌های جنگو بیشترین چالش را برای شما داشت — طراحی ارث‌بری، ساخت تگ‌های سفارشی، یا بهینه‌سازی عملکرد. تجربه خودتان را در دیدگاه‌ها بنویسید.