فصل ۴: View، URL و Template

تگ و فیلتر سفارشی: تاریخ شمسی، ارقام فارسی و قیمت تومانی

قالب فارسی بدون تکرار کد

در هر سایت ایرانی سه نیاز نمایشی همیشگی داریم: تاریخ شمسی، ارقام فارسی و مبلغ به تومان با جداکننده‌ی هزارگان. جای این منطق نه در مدل است نه در View؛ جایش فیلتر قالب است. فیلترها و تگ‌های سفارشی در پوشه‌ی templatetags یک اپ قرار می‌گیرند (با __init__.py) و نام فایل، نام کتابخانه برای {% load %} است.

# apps/core/templatetags/persian.py
import datetime

import jdatetime
from django import template
from django.utils import timezone

register = template.Library()

FA_DIGITS = str.maketrans("0123456789", "۰۱۲۳۴۵۶۷۸۹")
MONTHS = ["فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور",
          "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند"]


@register.filter
def fa_digits(value):
    return str(value).translate(FA_DIGITS)


@register.filter
def jdate(value, fmt="%Y/%m/%d"):
    if not value:
        return ""
    if isinstance(value, datetime.datetime):
        if timezone.is_aware(value):
            value = timezone.localtime(value)          # UTC به وقت تهران
        j = jdatetime.datetime.fromgregorian(datetime=value)
    else:
        j = jdatetime.date.fromgregorian(date=value)
    return j.strftime(fmt).translate(FA_DIGITS)


@register.filter
def jdate_long(value):
    if not value:
        return ""
    if isinstance(value, datetime.datetime):
        value = timezone.localtime(value).date()
    j = jdatetime.date.fromgregorian(date=value)
    return f"{j.day} {MONTHS[j.month - 1]} {j.year}".translate(FA_DIGITS)


@register.filter
def toman(rial):
    try:
        return f"{int(rial) // 10:,}".replace(",", "٬").translate(FA_DIGITS) + " تومان"
    except (TypeError, ValueError):
        return ""


@register.simple_tag(takes_context=True)
def active(context, url_name):
    match = context["request"].resolver_match
    return "active" if match and match.view_name == url_name else ""
{% load persian %}
<p>تاریخ ثبت: {{ order.created_at|jdate:"%Y/%m/%d %H:%M" }}</p>
<p>تحویل: {{ order.delivery_date|jdate_long }}</p>       {# ۱۵ بهمن ۱۴۰۴ #}
<p>مبلغ: {{ order.total|toman }}</p>                      {# ۱۲٬۵۰۰٬۰۰۰ تومان #}
<a class="{% active 'orders:list' %}" href="{% url 'orders:list' %}">سفارش‌ها</a>

inclusion_tag: یک تکه‌ی قالب قابل‌استفاده‌ی مجدد

@register.inclusion_tag("orders/_status_badge.html")
def status_badge(order):
    colors = {"pending": "warning", "paid": "info", "shipped": "success", "cancelled": "secondary"}
    return {"label": order.get_status_display(), "color": colors.get(order.status, "light")}

نکته‌هایی که کمتر کسی می‌داند

  • بعد از ساختن پوشه‌ی templatetags باید runserver را از نو اجرا کنید؛ کتابخانه‌های قالب فقط هنگام شروع ثبت می‌شوند و تا آن موقع خطای «is not a registered tag library» می‌گیرید.
  • اگر کتابخانه‌ای را در همه‌ی قالب‌ها لازم دارید، آن را در TEMPLATES["OPTIONS"]["builtins"] = ["apps.core.templatetags.persian"] بگذارید تا دیگر {% load %} لازم نباشد.
  • تبدیل تاریخ aware به شمسی بدون timezone.localtime زمان UTC را نشان می‌دهد؛ سفارش ساعت ۲ بامداد تهران در روز «قبل» نمایش داده می‌شود.
  • جداکننده‌ی هزارگان فارسی کاراکتر «٬» (U+066C) است نه ویرگول لاتین؛ در متن راست‌به‌چپ ویرگول لاتین گاهی جای عدد را به‌هم می‌ریزد.
  • فیلترها را برای ورودی نامعتبر مقاوم بنویسید (مثل try در toman)؛ خطای یک فیلتر کل صفحه را با 500 از کار می‌اندازد.

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