Skip to main content

django-jalali-suite

Jalali (Persian) calendar support for Django: model fields, forms, admin, template filters and Django REST Framework fields in one package.

The database always holds ordinary Gregorian date / timestamptz columns. Python code sees jdatetime values that compare, sort and subtract against standard date / datetime objects, keep their time zone, and agree with Django about local time.

The rule it follows

You decide which fields are Jalali: declare them as JalaliDateField / JalaliDateTimeField (or subclass a built-in field into one, as you would for last_login). With LANGUAGE_CODE = "fa", those fields are Jalali wherever the package shows them, with Persian month names (فروردین، اردیبهشت، ...) and Persian digits:

  • in the admin: inputs and datepicker, list columns, read-only values, filters, date_hierarchy
  • in forms: the same datepicker widget
  • in templates: the tags and filters the package provides (jalali, jalali_date, ...)

Nothing else changes. Ordinary Django date fields, the admin history page and every other page stay exactly as Django renders them, and the package overrides none of Django's own filters or settings. A value never depends on the language either: str(value) is the ISO string in every language. Only what the package displays follows the language.

Features

  • Model fields JalaliDateField / JalaliDateTimeField returning jdatetime values, with a type that is the same whether an instance was just created or loaded from the database
  • Time-zone correct: aware values are shown and edited in the current Django time zone; typed wall-clock time is saved as that local instant
  • Query lookups: __year (and __year__gt/gte/lt/lte) in the Jalali calendar, plus jalali_range() helpers for months and days
  • Forms: Jalali date, datetime and split date+time fields that clean to standard Gregorian values
  • Admin: Jalali datepicker widgets, Jalali list columns that stay sortable, read-only values, a Jalali date filter, Jalali date_hierarchy, inline support
  • Templates: the jalali, jalali_digits, jalali_now, jalali_date filters and tags
  • Django REST Framework serializer fields and JalaliModelSerializer
  • Persian / Arabic-Indic digit input and Farsi digit output, bundled Vazirmatn font, no JavaScript framework

Installation

pip install django-jalali-suite            # core
pip install "django-jalali-suite[drf]"     # with Django REST Framework support
INSTALLED_APPS = [
    # ...
    "jalali_suite",
]

Requires Python 3.10+, Django 5.2+ and jdatetime 6.1+.

Quick start

from datetime import date
from jalali_suite import format_jalali, to_gregorian, to_jalali

to_jalali(date(2024, 3, 20))                    # jdatetime.date(1403, 1, 1)
to_gregorian(1403, 1, 1)                        # datetime.date(2024, 3, 20)
format_jalali(date(2024, 3, 20), "%Y/%m/%d")    # '1403/01/01'
format_jalali(date(2024, 3, 20), digits="farsi")  # '۱۴۰۳/۰۱/۰۱'

Models

from django.db import models
from jalali_suite.models import JalaliDateField, JalaliDateTimeField

class Person(models.Model):
    birthday = JalaliDateField()
    created_at = JalaliDateTimeField(auto_now_add=True)
person = Person.objects.get(pk=1)
person.birthday              # jdatetime.date(1403, 1, 1)
person.birthday == date(2024, 3, 20)         # True
person.created_at < timezone.now()           # True  (aware, current time zone)
timezone.now() - person.created_at           # a timedelta
person.birthday = "1403-02-15"               # strings, dates, datetimes all work

Every assignment — constructor arguments, default=, auto_now, forms, refresh_from_db() — is normalised to a jdatetime object, so an attribute never changes type between "fresh" and "loaded".

Time zones. With USE_TZ = True an aware value is converted to the current time zone when read, exactly as Django does for its own DateTimeField. A Jalali string without an offset ("1403-01-01 15:30") means local wall-clock time. Naive datetime objects keep Django's usual behaviour: they are interpreted in the default time zone and Django's RuntimeWarning is raised.

Serialization. dumpdata/loaddata use Gregorian ISO strings, so fixtures do not depend on this package.

Querying

Person.objects.filter(birthday=jdatetime.date(1403, 1, 1))
Person.objects.filter(birthday="1403-01-01")           # Jalali string
Person.objects.filter(birthday__gte=date(2024, 3, 20))  # Gregorian works too

Person.objects.filter(birthday__year=1403)              # Jalali year
Person.objects.filter(birthday__year__gte=1400)
Person.objects.filter(created_at__date="1403-01-01")    # local Jalali date of a datetime
Person.objects.filter(created_at__date__year=1403)

__year is answered with a half-open Gregorian range, so it works on every database and can use an index. __month, __day and __quarter raise FieldError instead of silently using the Gregorian calendar; use a range:

from jalali_suite import jalali_datetime_range, jalali_range

start, end = jalali_range(1403, 7)             # Mehr 1403, as Gregorian dates
Person.objects.filter(birthday__gte=start, birthday__lt=end)

start, end = jalali_datetime_range(1403, 7)    # local-midnight datetimes
Event.objects.filter(at__gte=start, at__lt=end)

Where Django still uses the Gregorian calendar

Django's own calendar functions know nothing about Jalali dates and always use the Gregorian calendar, even on Jalali fields: ExtractYear / ExtractMonth / ExtractDay, TruncYear / TruncMonth, QuerySet.dates() and QuerySet.datetimes(). They return no error, so do not use them for Jalali grouping. Use jalali_dates() instead of dates() / datetimes():

from jalali_suite.query import jalali_dates

jalali_dates(Order.objects.all(), "created_at", "year")
# [jdatetime.date(1402, 1, 1), jdatetime.date(1403, 1, 1)]
jalali_dates(Order.objects.filter(...), "created_at", "month")   # month starts

It works for Jalali fields and plain Django date/datetime fields (datetimes are read in the current time zone).

Forms

from django import forms
from jalali_suite.forms import JalaliDateField, SplitJalaliDateTimeField

class EventForm(forms.Form):
    day = JalaliDateField()
    at = SplitJalaliDateTimeField()

Input may use Latin, Persian or Arabic-Indic digits and - or / separators. Input is always read as Jalali. Fields clean to standard values — datetime.date and an aware datetime.datetime — so they work with any model field, including plain Django ones. ModelForms for the suite's model fields use these fields automatically. Render {{ form.media }} in the page head to load the datepicker and bundled font.

Django admin

from django.contrib import admin
from jalali_suite.admin import JalaliDateModelAdmin

@admin.register(Person)
class PersonAdmin(JalaliDateModelAdmin):
    list_display = ["id", "birthday", "created_at"]
    list_filter = ["birthday"]

or add the mixin to your own admin class: class PersonAdmin(JalaliDateAdminMixin, admin.ModelAdmin).

What you get:

  • the Jalali datepicker for date fields and a split date + time widget for datetime fields
  • list columns and read-only fields shown as Jalali text (list columns remain sortable); switch off with LIST_DISPLAY_AUTO_CONVERT
  • a Jalali date filter (Today, Past 7 days, This month, This year using Jalali month and year boundaries), applied automatically to the suite's fields
  • date_hierarchy drill-down by Jalali year, month and day, in the right time zone, for the suite's fields and plain Django date columns alike. Django's own hierarchy would filter on Gregorian numbers, so a system check (jalali_suite.W001) warns if an admin uses a Jalali field with date_hierarchy but no JalaliDateAdminMixin. If you set your own change_list_template, put {% load jalali_suite %}{% jalali_date_hierarchy cl %} in its date_hierarchy block. Opt out with jalali_date_hierarchy = False
  • plain Django DateField / DateTimeField columns are left alone. To get the same treatment for them on one admin, set jalali_widgets_for_gregorian_fields = True on it; the date filter is then used explicitly: list_filter = [("created", JalaliDateFieldListFilter)]
  • inline support: JalaliDateTabularInline, JalaliDateStackedInline, or the mixin on your own inline

The datepicker needs no third-party JavaScript. Vazirmatn Regular is bundled (SIL OFL), so nothing is fetched from external servers.

Templates

Display a Jalali value with the package's filters and tags, after {% load jalali_suite %}. With fa active they write Persian month names and digits:

{% load jalali_suite %}
{{ order.created|jalali }}                    {# ۱۴۰۳/۰۱/۰۱ ۱۵:۳۰:۰۰  (JALALI_SUITE format) #}
{{ order.created|jalali:"%d %B %Y" }}         {# ۰۱ فروردین ۱۴۰۳       (strftime format) #}
{{ order.created|jalali:"%A" }}               {# چهارشنبه #}
{{ order.created|jalali_digits:"farsi" }}
{% jalali_date order.created "%d %B %Y" %}
{% jalali_now "%Y/%m/%d" %}

Django's own |date and |time filters do not know the Jalali calendar. On a Jalali value they print a Gregorian month name next to a Jalali year (for example "ژانویه ۱۴۰۳") and number weekdays from the wrong day. The package does not change Django's filters; use |jalali / {% jalali_date %} for Jalali fields, and keep |date for ordinary dates. A bare {{ order.created }} prints the ISO value (1403-01-01) in every language.

For machines, convert to Gregorian first. <time datetime>, meta tags, feeds, JSON-LD and sitemaps must carry standard dates, but .isoformat() (and |date:"c") on a Jalali value prints the Jalali year. |gregorian returns the standard value, which Django's filters understand; in Python use to_gregorian_value():

<time datetime="{{ post.created|gregorian|date:'c' }}">{{ post.created|jalali:"%d %B %Y" }}</time>
from jalali_suite import to_gregorian_value

to_gregorian_value(post.created).isoformat()   # 2024-03-20T12:30:00+03:30

Aware datetimes are shown in the current time zone. Digits follow the active language (Farsi digits when it starts with fa); override per call with digits=.

Django REST Framework

from jalali_suite.serializers import JalaliModelSerializer

class PersonSerializer(JalaliModelSerializer):
    class Meta:
        model = Person
        fields = ("birthday", "created_at")
  • Output is Jalali ISO: "1403-01-01" and "1403-01-01T15:30:00+03:30" (local time, with offset).
  • Input accepts Jalali strings (Latin, Persian or Arabic-Indic digits) and Gregorian ISO strings: a year below 1700 is read as Jalali, anything else as Gregorian, so 2024-03-20 is never misread as the Jalali year 2024. Values with an offset (Z, +03:30) keep their instant; without one they are local time.
  • validated_data holds standard date / aware datetime objects.
  • A datetime must include a time; date-only strings belong in JalaliDateField.
  • JalaliDateSerializerField / JalaliDateTimeSerializerField can be used on plain Django columns.

Configuration

JALALI_SUITE = {
    "DATE_FORMAT": "%Y/%m/%d",
    "DATETIME_FORMAT": "%Y/%m/%d %H:%M:%S",
    "LIST_DISPLAY_AUTO_CONVERT": True,  # Jalali text for admin list and read-only date fields
}

Development

git clone https://github.com/omidshojaee/django-jalali-suite.git
cd django-jalali-suite
python -m pip install -e ".[dev]"
pytest

The test project runs with TIME_ZONE = "Asia/Tehran" on purpose.

License

MIT © Omid Shojaee

Metadata

Release files for django-jalali-suite 4.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-jalali-suite 4.2.0
File Size Uploaded
django_jalali_suite-4.2.0.tar.gz 95.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-jalali-suite 4.2.0
File Interpreter ABI Platform
django_jalali_suite-4.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 179.0 kB

Release files / django_jalali_suite-4.2.0.tar.gz

Download URL django_jalali_suite-4.2.0.tar.gz
Size 95.5 kB
Tags Source
SHA-256 checksum
How to use checksums
7eaa4a47a11c2f406ab098923c1ab1442157318b4ecb73af4af0a59929ce6a1b
BLAKE2b-256 checksum
How to use checksums
67a2d8e3aa4443af2bf1f688eded1f21d1296ff3f67174f8312540230cd90037
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / django_jalali_suite-4.2.0-py3-none-any.whl

Download URL django_jalali_suite-4.2.0-py3-none-any.whl
Size 83.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e3ef8506ec9b3b5b09c79c763d9fc98b353e6ef946026b0b92f544d945f57c40
BLAKE2b-256 checksum
How to use checksums
3a784ec3d5fcedc6dac12ed297bb00fc118fd3ddb197063743a243e473e4b9a0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

4.2.0 This release

2 release files

4.1.0

2 release files

4.0.0

2 release files

3.0.0

2 release files

2.0.10

2 release files

2.0.9

2 release files

2.0.8

2 release files

2.0.7

2 release files

2.0.6

2 release files

2.0.5

2 release files

2.0.4

2 release files

2.0.3

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page