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.
It is designed as a single replacement for django-jalali + django-jalali-date; see Migrating.
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/JalaliDateTimeFieldreturningjdatetimevalues, 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, plusjalali_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_datefilters and tags (andjformat/to_jalalialiases) - 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_hierarchydrill-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 withdate_hierarchybut noJalaliDateAdminMixin. If you set your ownchange_list_template, put{% load jalali_suite %}{% jalali_date_hierarchy cl %}in itsdate_hierarchyblock. Opt out withjalali_date_hierarchy = False- plain Django
DateField/DateTimeFieldcolumns are left alone. To get the same treatment for them on one admin (for example when moving from django-jalali-date), setjalali_widgets_for_gregorian_fields = Trueon 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=.
jformat and to_jalali are accepted as aliases of jalali.
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-20is never misread as the Jalali year 2024. Values with an offset (Z,+03:30) keep their instant; without one they are local time. validated_dataholds standarddate/ awaredatetimeobjects.- A datetime must include a time; date-only strings belong in
JalaliDateField. JalaliDateSerializerField/JalaliDateTimeSerializerFieldcan 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
}
Migrating from django-jalali and django-jalali-date
| You used | Use now |
|---|---|
django_jalali.db.models.jDateField / jDateTimeField |
jalali_suite.models.JalaliDateField / JalaliDateTimeField (aliases jDateField, jDateTimeField) |
django_jalali.admin.filters.JDateFieldListFilter |
jalali_suite.admin.JalaliDateFieldListFilter (alias JDateFieldListFilter) |
jalali_date.admin.ModelAdminJalaliMixin |
jalali_suite.admin.JalaliDateAdminMixin (alias ModelAdminJalaliMixin) |
TabularInlineJalaliMixin, StackedInlineJalaliMixin |
JalaliDateTabularInline, JalaliDateStackedInline (aliases for the mixin names exist) |
django_jalali.db.models.jManager / jQuerySet |
not needed; __year and __date work with the default manager (jManager / jQuerySet are importable aliases of Django's Manager / QuerySet) |
django_jalali.forms.jDateField / jDateTimeField / jDateInput / jDateTimeInput |
jalali_suite.forms and jalali_suite.widgets, same alias names |
django_jalali.serializers.serializerfield.JDateField / JDateTimeField |
jalali_suite.serializers (same alias names) |
jalali_date.fields.JalaliDateField, SplitJalaliDateTimeField |
jalali_suite.forms.JalaliDateField, SplitJalaliDateTimeField (takes input_date_formats / input_time_formats) |
jalali_date.widgets.AdminJalaliDateWidget, AdminSplitJalaliDateTime |
jalali_suite.widgets.AdminJalaliDateWidget, AdminJalaliSplitDateTimeWidget (alias AdminSplitJalaliDateTime) |
{{ v|jformat:"..." }} |
same filter, after {% load jalali_suite %}; renders "" for empty values as before |
{{ v|to_jalali:"..." }}, {% jalali_now %} |
same, rendering - for empty values as before; jalali_now now respects the time zone |
jalali_admin_safe_readonly filter and the jalali_* admin templates |
not needed: read-only date fields are shown as Jalali by the admin mixin itself |
datetime2jalali(), date2jalali() |
same names in jalali_suite; None in, None out |
Settings:
| You set | Use now |
|---|---|
JALALI_DATE_DEFAULTS["LIST_DISPLAY_AUTO_CONVERT"] (default False) |
JALALI_SUITE["LIST_DISPLAY_AUTO_CONVERT"] (default True); also controls read-only fields |
JALALI_DATE_DEFAULTS["Strftime"]["date"] / ["datetime"] |
JALALI_SUITE["DATE_FORMAT"] / ["DATETIME_FORMAT"] |
JALALI_DATE_DEFAULTS["Static"], JALALI_SETTINGS["ADMIN_JS_STATIC_FILES"] / ["ADMIN_CSS_STATIC_FILES"] |
not supported: the package ships its own datepicker (the django-jalali one used jQuery UI). Subclass the widgets and override Media to use another one |
Things to know when switching:
- Migrations. Existing migrations that reference
django_jalali.db.models.jDateFieldmust be edited tojalali_suite.models.JalaliDateField. No schema change is needed: the columns are the same. - Lookups.
__yearand__datework on the Jalali calendar (no custom manager needed).__month,__dayand__quarterraiseFieldError, as__monthetc. also do in django-jalali; usejalali_range()with__gte/__lt. - Form field return types. Form fields return Gregorian
date/ awaredatetime(as django-jalali-date does) so they work with any model field. django-jalali'sjDateFieldform field returned ajdatetime.date; code that readscleaned_datadirectly and relied on that should callto_jalali(). Saving through aModelFormis unaffected. - Date-only datetimes. A datetime serializer or form input must include a time; django-jalali's
JDateTimeFieldserializer also accepted a bare date. - Plain date fields. django-jalali-date's mixin put Jalali widgets on every date field. Here only the suite's own fields get them; add
jalali_widgets_for_gregorian_fields = Trueto an admin to keep the old behaviour. - List columns on by default. Unlike django-jalali-date, the admin mixin converts the Jalali fields' list columns and read-only values unless you set
LIST_DISPLAY_AUTO_CONVERTtoFalse. - Remove
django_jalali/jalali_datefromINSTALLED_APPSand addjalali_suite.
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.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_jalali_suite-4.1.0.tar.gz | 97.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_jalali_suite-4.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 183.0 kB
Release files / django_jalali_suite-4.1.0.tar.gz
| Download URL | django_jalali_suite-4.1.0.tar.gz |
|---|---|
| Size | 97.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a8779d7f2d518519ee278954b4bf788b1c062f0cf6a071418ef1fe23f473bf13
|
|
BLAKE2b-256 checksum How to use checksums |
5e06625bb919ec2cdf48dc8e5a57d5bdef693232c2f7df43928ce3838e604b3e
|
| 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 11, 2026.
Transparency logRelease files / django_jalali_suite-4.1.0-py3-none-any.whl
| Download URL | django_jalali_suite-4.1.0-py3-none-any.whl |
|---|---|
| Size | 85.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
449d6a46ed6dd23dc2bff2b34eb64c05e95211c8036b7c7ae6259c48ea7ffbc0
|
|
BLAKE2b-256 checksum How to use checksums |
0bdbd0e49d6bb606d231e7912ebd67b31907c13be42745a507ba05f61b48c083
|
| 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 11, 2026.
Transparency log