Features: 1) Migrate from Jazzmin to django-unfold for admin UI;

Fixes: 1) Remove deprecated Jazzmin configuration and replace with unfold dependencies; 2) Update DRF API title to use new PROJECT_NAME; 3) Fix import order and remove unused imports in core/viewsets.py;

Extra: 1) Add PROJECT_NAME to base settings; 2) Update INSTALLED_APPS to include unfold-related apps; 3) Clean up unused config references.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-11-15 01:38:14 +03:00
parent fb84f1f89b
commit 43dc556063
18 changed files with 122 additions and 192 deletions

View file

@ -217,21 +217,6 @@ services:
- --web.config.file=/etc/prometheus/web.yml
logging: *default-logging
# supervisor:
# container_name: supervisor
# build:
# context: .
# dockerfile: ./Dockerfiles/supervisor.Dockerfile
# restart: always
# env_file:
# - .env
# ports:
# - "7777:7777"
# depends_on:
# app:
# condition: service_started
# logging: *default-logging
volumes:
postgres-data:

View file

@ -993,6 +993,6 @@ class ConstanceConfig:
site.unregister([Config])
# noinspection PyTypeChecker
site.register([ConstanceConfig], BaseConstanceAdmin) # type: ignore [list-item]
site.site_title = settings.CONSTANCE_CONFIG["PROJECT_NAME"][0] # type: ignore [assignment]
site.site_title = settings.PROJECT_NAME
site.site_header = "eVibes"
site.index_title = settings.CONSTANCE_CONFIG["PROJECT_NAME"][0] # type: ignore [assignment]
site.index_title = settings.PROJECT_NAME

View file

@ -2,7 +2,6 @@ import logging
from contextlib import suppress
from typing import Any
from constance import config
from django.conf import settings
from django.core.cache import cache
from django.db.models import Max, Min, QuerySet
@ -139,7 +138,7 @@ class BrandType(DjangoObjectType): # type: ignore [misc]
lang = graphene_current_lang()
base = f"https://{settings.BASE_DOMAIN}"
canonical = f"{base}/{lang}/brand/{self.slug}"
title = f"{self.name} | {config.PROJECT_NAME}"
title = f"{self.name} | {settings.PROJECT_NAME}"
description = (self.description or "")[:180]
logo_url = None
@ -265,7 +264,7 @@ class CategoryType(DjangoObjectType): # type: ignore [misc]
lang = graphene_current_lang()
base = f"https://{settings.BASE_DOMAIN}"
canonical = f"{base}/{lang}/catalog/{self.slug}"
title = f"{self.name} | {config.PROJECT_NAME}"
title = f"{self.name} | {settings.PROJECT_NAME}"
description = (self.description or "")[:180]
og_image = graphene_abs(info.context, self.image.url) if getattr(self, "image", None) else ""
@ -537,7 +536,7 @@ class ProductType(DjangoObjectType): # type: ignore [misc]
lang = graphene_current_lang()
base = f"https://{settings.BASE_DOMAIN}"
canonical = f"{base}/{lang}/product/{self.slug}"
title = f"{self.name} | {config.PROJECT_NAME}"
title = f"{self.name} | {settings.PROJECT_NAME}"
description = (self.description or "")[:180]
first_img = self.images.order_by("priority").first()

View file

@ -30,4 +30,5 @@ class DRFCoreViewsTests(TestCase):
serializer.is_valid(raise_exception=True)
return serializer.validated_data["access_token"]
# TODO: create tests for every possible HTTP method in core module with DRF stack

View file

@ -24,7 +24,7 @@ def contact_us_email(contact_info) -> tuple[bool, str]:
)
email = EmailMessage(
_(f"{config.PROJECT_NAME} | contact us initiated"),
_(f"{settings.PROJECT_NAME} | contact us initiated"),
render_to_string(
"../templates/contact_us_email.html",
{
@ -37,7 +37,7 @@ def contact_us_email(contact_info) -> tuple[bool, str]:
},
),
to=[config.EMAIL_FROM],
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
from_email=f"{settings.PROJECT_NAME} <{config.EMAIL_FROM}>",
connection=get_dynamic_email_connection(),
)
email.content_subtype = "html"
@ -70,7 +70,7 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]:
if not order.is_whole_digital:
email = EmailMessage(
_(f"{config.PROJECT_NAME} | order confirmation"),
_(f"{settings.PROJECT_NAME} | order confirmation"),
render_to_string(
"digital_order_created_email.html" if order.is_whole_digital else "shipped_order_created_email.html",
{
@ -81,7 +81,7 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]:
},
),
to=[recipient],
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
from_email=f"{settings.PROJECT_NAME} <{config.EMAIL_FROM}>",
connection=get_dynamic_email_connection(),
)
email.content_subtype = "html"
@ -102,14 +102,14 @@ def send_order_finished_email(order_pk: str) -> tuple[bool, str]:
activate(order.user.language)
email = EmailMessage(
_(f"{config.PROJECT_NAME} | order delivered"),
_(f"{settings.PROJECT_NAME} | order delivered"),
render_to_string(
template_name="../templates/digital_order_delivered_email.html",
context={
"order_uuid": order.human_readable_id,
"user_first_name": "" or order.user.first_name,
"order_products": ops,
"project_name": config.PROJECT_NAME,
"project_name": settings.PROJECT_NAME,
"contact_email": config.EMAIL_FROM,
"total_price": round(sum(0.0 or op.buy_price for op in ops), 2), # type: ignore [misc]
"display_system_attributes": order.user.has_perm("core.view_order"),
@ -117,7 +117,7 @@ def send_order_finished_email(order_pk: str) -> tuple[bool, str]:
},
),
to=[order.user.email],
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
from_email=f"{settings.PROJECT_NAME} <{config.EMAIL_FROM}>",
connection=get_dynamic_email_connection(),
)
email.content_subtype = "html"
@ -185,20 +185,20 @@ def send_promocode_created_email(promocode_pk: str) -> tuple[bool, str]:
activate(promocode.user.language)
email = EmailMessage(
_(f"{config.PROJECT_NAME} | promocode granted"),
_(f"{settings.PROJECT_NAME} | promocode granted"),
render_to_string(
template_name="../templates/promocode_granted_email.html",
context={
"promocode": promocode,
"user_first_name": "" or promocode.user.first_name,
"project_name": config.PROJECT_NAME,
"project_name": settings.PROJECT_NAME,
"contact_email": config.EMAIL_FROM,
"today": datetime.today(),
"currency": settings.CURRENCY_CODE,
},
),
to=[promocode.user.email],
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
from_email=f"{settings.PROJECT_NAME} <{config.EMAIL_FROM}>",
connection=get_dynamic_email_connection(),
)
email.content_subtype = "html"

View file

@ -18,7 +18,7 @@ def website_schema():
return {
"@context": "https://schema.org",
"@type": "WebSite",
"name": config.PROJECT_NAME,
"name": settings.PROJECT_NAME,
"url": f"https://{settings.BASE_DOMAIN}/",
"potentialAction": {
"@type": "SearchAction",

View file

@ -3,9 +3,8 @@ import uuid
from typing import Type
from uuid import UUID
from constance import config
from django.conf import settings
from django.db.models import Prefetch, Q, OuterRef, Exists
from django.db.models import Exists, OuterRef, Prefetch, Q
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
@ -270,7 +269,7 @@ class CategoryViewSet(EvibesViewSet):
def seo_meta(self, request: Request, *args, **kwargs) -> Response:
category = self.get_object()
title = f"{category.name} | {config.PROJECT_NAME}"
title = f"{category.name} | {settings.PROJECT_NAME}"
description = (category.description or "")[:180]
canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{category.slug}"
og_image = request.build_absolute_uri(category.image.url) if getattr(category, "image", None) else ""
@ -387,7 +386,7 @@ class BrandViewSet(EvibesViewSet):
def seo_meta(self, request: Request, *args, **kwargs) -> Response:
brand = self.get_object()
title = f"{brand.name} | {config.PROJECT_NAME}"
title = f"{brand.name} | {settings.PROJECT_NAME}"
description = (brand.description or "")[:180]
canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/brand/{brand.slug}"
@ -529,7 +528,7 @@ class ProductViewSet(EvibesViewSet):
p = self.get_object()
images = list(p.images.all()[:6])
rating = {"value": p.rating, "count": p.feedbacks_count}
title = f"{p.name} | {config.PROJECT_NAME}"
title = f"{p.name} | {settings.PROJECT_NAME}"
description = (p.description or "")[:180]
canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/product/{p.slug}"
og = {

View file

@ -2,6 +2,7 @@ from datetime import datetime
from celery.app import shared_task
from constance import config
from django.conf import settings
from django.core.mail import EmailMessage
from django.template.loader import render_to_string
from django.utils.translation import activate
@ -24,20 +25,20 @@ def balance_deposit_email(transaction_pk: str) -> tuple[bool, str]:
activate(transaction.balance.user.language)
email = EmailMessage(
_(f"{config.PROJECT_NAME} | balance deposit"),
_(f"{settings.PROJECT_NAME} | balance deposit"),
render_to_string(
template_name="../templates/balance_deposit_email.html",
context={
"amount": transaction.amount,
"balance": transaction.balance.amount,
"user_first_name": transaction.balance.user.first_name,
"project_name": config.PROJECT_NAME,
"project_name": settings.PROJECT_NAME,
"contact_email": config.EMAIL_FROM,
"today": datetime.today(),
},
),
to=[transaction.balance.user.email],
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
from_email=f"{settings.PROJECT_NAME} <{config.EMAIL_FROM}>",
connection=get_dynamic_email_connection(),
)
email.content_subtype = "html"

View file

@ -21,21 +21,21 @@ def send_verification_email_task(user_pk: str) -> tuple[bool, str]:
activate(user.language)
email_subject = _(f"{config.PROJECT_NAME} | Activate Account")
email_subject = _(f"{settings.PROJECT_NAME} | Activate Account")
email_body = render_to_string(
"../templates/user_verification_email.html",
{
"user_first_name": user.first_name,
"activation_link": f"https://{settings.STOREFRONT_DOMAIN}/{user.language}/activate-user?uid={urlsafe_base64_encode(force_bytes(user.uuid))}"
f"&token={urlsafe_base64_encode(force_bytes(user.activation_token))}",
"project_name": config.PROJECT_NAME,
"project_name": settings.PROJECT_NAME,
},
)
email = EmailMessage(
subject=email_subject,
body=email_body,
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
from_email=f"{settings.PROJECT_NAME} <{config.EMAIL_FROM}>",
to=[user.email],
connection=get_dynamic_email_connection(),
)
@ -60,7 +60,7 @@ def send_reset_password_email_task(user_pk: str) -> tuple[bool, str]:
activate(user.language)
email_subject = _(f"{config.PROJECT_NAME} | Reset Password")
email_subject = _(f"{settings.PROJECT_NAME} | Reset Password")
email_body = render_to_string(
"../templates/user_reset_password_email.html",
{
@ -68,14 +68,14 @@ def send_reset_password_email_task(user_pk: str) -> tuple[bool, str]:
"reset_link": f"https://{settings.STOREFRONT_DOMAIN}/{user.language}/reset-password?uid="
f"{urlsafe_base64_encode(force_bytes(user.pk))}"
f"&token={PasswordResetTokenGenerator().make_token(user)}",
"project_name": config.PROJECT_NAME,
"project_name": settings.PROJECT_NAME,
},
)
email = EmailMessage(
subject=email_subject,
body=email_body,
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
from_email=f"{settings.PROJECT_NAME} <{config.EMAIL_FROM}>",
to=[user.email],
connection=get_dynamic_email_connection(),
)

View file

@ -9,6 +9,6 @@ from .elasticsearch import * # noqa: F403
from .emailing import * # noqa: F403
from .extensions import * # noqa: F403
from .graphene import * # noqa: F403
from .jazzmin import * # noqa: F403
from .unfold import * # noqa: F403
from .logconfig import * # noqa: F403
from .summernote import * # noqa: F403

View file

@ -9,6 +9,8 @@ from django.core.exceptions import ImproperlyConfigured
EVIBES_VERSION = "2025.4"
RELEASE_DATE = datetime(2025, 11, 9)
PROJECT_NAME = getenv("EVIBES_PROJECT_NAME", "eVibes")
BASE_DIR: Path = Path(__file__).resolve().parent.parent.parent
INITIALIZED: bool = (BASE_DIR / ".initialized").exists()
@ -103,7 +105,9 @@ SITE_ID: int = 1
INSTALLED_APPS: list[str] = [
"django_prometheus",
"constance",
"jazzmin",
"unfold",
"unfold.contrib.filters",
"unfold.contrib.forms",
"modeltranslation",
"django.contrib.admin",
"django.contrib.admindocs",

View file

@ -1,8 +1,7 @@
from collections import OrderedDict
from os import getenv
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_noop
from django.utils.translation import gettext_noop as _
CONSTANCE_BACKEND = "constance.backends.database.DatabaseBackend"
CONSTANCE_SUPERUSER_ONLY = False
@ -19,11 +18,11 @@ CONSTANCE_ADDITIONAL_FIELDS = {
CONSTANCE_CONFIG = OrderedDict(
[
### General Options ###
("PROJECT_NAME", (getenv("EVIBES_PROJECT_NAME"), _("Name of the project"))),
### Legal Options ###
("COMPANY_NAME", (getenv("COMPANY_NAME"), _("Name of the company"))),
("COMPANY_ADDRESS", (getenv("COMPANY_ADDRESS"), _("Address of the company"))),
("COMPANY_PHONE_NUMBER", (getenv("COMPANY_PHONE_NUMBER"), _("Phone number of the company"))),
("TAX_RATE", (0, _("Tax rate in jurisdiction of your company. Leave 0 if you don't want to process taxes."))),
("EXCHANGE_RATE_API_KEY", (getenv("EXCHANGE_RATE_API_KEY", "example token"), _("Exchange rate API key"))),
### Email Options ###
("EMAIL_BACKEND", ("django.core.mail.backends.smtp.EmailBackend", _("!!!DO NOT CHANGE!!!"))),
@ -52,14 +51,14 @@ CONSTANCE_CONFIG = OrderedDict(
CONSTANCE_CONFIG_FIELDSETS = OrderedDict(
{
gettext_noop("General Options"): (
"PROJECT_NAME",
_("Legal Options"): (
"COMPANY_NAME",
"COMPANY_ADDRESS",
"COMPANY_PHONE_NUMBER",
"TAX_RATE",
"EXCHANGE_RATE_API_KEY",
),
gettext_noop("Email Options"): (
_("Email Options"): (
"EMAIL_BACKEND",
"EMAIL_HOST",
"EMAIL_PORT",
@ -69,7 +68,7 @@ CONSTANCE_CONFIG_FIELDSETS = OrderedDict(
"EMAIL_HOST_PASSWORD",
"EMAIL_FROM",
),
gettext_noop("Features Options"): (
_("Features Options"): (
"DAYS_TO_STORE_ANON_MSGS",
"DAYS_TO_STORE_AUTH_MSGS",
"DISABLED_COMMERCE",
@ -78,16 +77,15 @@ CONSTANCE_CONFIG_FIELDSETS = OrderedDict(
"ABSTRACT_API_KEY",
"HTTP_PROXY",
),
gettext_noop("SEO Options"): (
_("SEO Options"): (
"ADVERTSIMENT",
"ANALYTICS",
),
gettext_noop("Debugging Options"): ("SAVE_VENDORS_RESPONSES",),
_("Debugging Options"): ("SAVE_VENDORS_RESPONSES",),
}
)
EXPOSABLE_KEYS = [
"PROJECT_NAME",
"COMPANY_NAME",
"COMPANY_ADDRESS",
"COMPANY_PHONE_NUMBER",

View file

@ -3,8 +3,7 @@ from os import getenv
from django.utils.translation import gettext_lazy as _
from evibes.settings.base import DEBUG, EVIBES_VERSION, SECRET_KEY, BASE_DOMAIN
from evibes.settings.constance import CONSTANCE_CONFIG
from evibes.settings.base import BASE_DOMAIN, DEBUG, EVIBES_VERSION, PROJECT_NAME, SECRET_KEY
REST_FRAMEWORK: dict[str, str | int | list[str] | tuple[str, ...] | dict[str, bool]] = {
"DEFAULT_PAGINATION_CLASS": "evibes.pagination.CustomPagination",
@ -99,7 +98,7 @@ Current API version: {EVIBES_VERSION}
SPECTACULAR_SETTINGS = {
"DEFAULT_GENERATOR_CLASS": "drf_spectacular_websocket.schemas.WsSchemaGenerator",
"TITLE": f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]} API", # type: ignore [index]
"TITLE": f"{PROJECT_NAME} API", # type: ignore [index]
"DESCRIPTION": SPECTACULAR_DESCRIPTION,
"VERSION": EVIBES_VERSION, # noqa: F405
"TOS": "https://evibes.wiseless.xyz/terms-of-service",

View file

@ -1,84 +1,11 @@
from django.utils.translation import gettext_lazy as _
"""
Deprecated: Jazzmin settings (removed in favor of django-unfold).
from evibes.settings.base import EVIBES_VERSION, STOREFRONT_DOMAIN
from evibes.settings.constance import CONSTANCE_CONFIG
This file is intentionally left as a stub to avoid accidental imports.
If imported, raise an explicit error guiding developers to Unfold.
"""
JAZZMIN_SETTINGS = {
"site_title": f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]} Admin", # type: ignore [index]
"site_header": str(CONSTANCE_CONFIG.get("PROJECT_NAME")[0]), # type: ignore [index]
"site_brand": str(CONSTANCE_CONFIG.get("PROJECT_NAME")[0]), # type: ignore [index]
"site_logo": "logo.png",
"login_logo": "logo.png",
"login_logo_dark": "logo.png",
"site_logo_classes": "",
"site_icon": "favicon.ico",
"welcome_sign": _("Whoa! Only admins allowed here!"),
"copyright": f"eVibes {EVIBES_VERSION} by Wiseless",
"search_model": None,
"user_avatar": "avatar",
"topmenu_links": [
{
"name": _("Home"),
"url": "admin:index",
"new_window": False,
},
{
"name": _("Storefront"),
"url": f"https://{STOREFRONT_DOMAIN}",
"new_window": False,
},
{
"name": "GraphQL Docs",
"url": "graphql-platform",
"new_window": False,
},
{
"name": "Swagger",
"url": "swagger-ui-platform",
"new_window": False,
},
{
"name": "Redoc",
"url": "redoc-ui-platform",
"new_window": False,
},
{
"name": _("Taskboard"),
"url": "https://plane.wiseless.xyz/spaces/issues/dd33cb0ab9b04ef08a10f7eefae6d90c/?board=kanban",
"new_window": True,
},
{
"name": "GitLab",
"url": "https://gitlab.com/wiseless/evibes",
"new_window": True,
},
{
"name": _("Support"),
"url": "https://t.me/fureunoir",
"new_window": True,
},
],
"usermenu_links": [],
"show_sidebar": True,
"navigation_expanded": True,
"hide_apps": ["django_celery_results", ""],
"hide_models": [],
"order_with_respect_to": ["vibes_auth", "core", "payments", "blog"],
"icons": {
"vibes_auth": "fas fa-users-cog",
"vibes_auth.user": "fas fa-user",
"vibes_auth.Group": "fas fa-users",
},
"default_icon_parents": "fas fa-chevron-circle-right",
"default_icon_children": "fas fa-circle",
"related_modal_active": False,
"use_google_fonts_cdn": True,
"show_ui_builder": True,
"changeform_format": "horizontal_tabs",
"language_chooser": True,
}
JAZZMIN_UI_TWEAKS = {
"theme": "flatly",
"dark_mode_theme": "darkly",
}
raise ImportError(
"Jazzmin configuration has been removed. Use django-unfold instead. "
"See evibes/settings/unfold.py and INSTALLED_APPS in evibes/settings/base.py."
)

28
evibes/settings/unfold.py Normal file
View file

@ -0,0 +1,28 @@
"""django-unfold configuration.
This module defines branding for the Django admin using django-unfold.
It intentionally avoids database-backed configuration (e.g., Constance)
so that it is safe during initial migrations and in all environments.
"""
from evibes.settings.base import PROJECT_NAME
# See django-unfold documentation for all available options.
# Only minimal, production-safe branding is configured here.
UNFOLD = {
# Text shown in the browser title bar and in the admin header
"SITE_TITLE": f"{PROJECT_NAME} Admin",
"SITE_HEADER": PROJECT_NAME,
# Optional URL the header/brand links to (leave default admin index)
# "SITE_URL": "/admin/",
# Logos and favicon served via Django staticfiles
# Files are expected at: engine/core/static/logo.png, favicon.ico, favicon.png
# Refer to them by their static URL path (relative), no leading slash.
"SITE_LOGO": "logo.png",
# If you use a different logo for dark theme, set SITE_LOGO_DARK
# Otherwise Unfold will reuse SITE_LOGO
# "SITE_LOGO_DARK": "logo.png",
"SITE_ICON": "favicon.ico",
# Sidebar behavior, search etc. (keep defaults minimal)
# Unfold automatically respects user OS light/dark theme; no forcing here.
}

View file

@ -23,7 +23,6 @@ dependencies = [
"django-extensions==4.1",
"django-filter==25.2",
"django-health-check==3.20.0",
"django-jazzmin==3.0.1",
"django-json-widget==2.1.0",
"django-mailbox==4.10.1",
"django-model-utils==5.0.0",
@ -35,6 +34,7 @@ dependencies = [
"django-storages==1.14.6",
"django-stubs==5.2.7",
"django-summernote==0.8.20.0",
"django-unfold>=0.71.0",
"django-widget-tweaks==1.5.0",
"django-md-field==0.1.0",
"djangorestframework==3.16.1",
@ -87,7 +87,7 @@ linting = [
"isort==7.0.0",
"mypy==1.18.2",
"mypy-extensions==1.1.0",
"ruff==0.14.4",
"ruff==0.14.5",
"celery-stubs==0.1.3",
]
openai = ["openai==2.6.1"]

View file

@ -1,11 +0,0 @@
{
"name": "supervisor",
"version": "1.0.0",
"description": "Supervisor is a custom dashboard application for eVibes",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "WISELESS team",
"license": "../LICENSE"
}

70
uv.lock
View file

@ -874,18 +874,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/7f/49ba63f078015b0a52e09651b94ba16b41154ac7079c83153edd14e15ca0/django_health_check-3.20.0-py3-none-any.whl", hash = "sha256:bcb2b8f36f463cead0564a028345c5b17e2a2d18e9cc88ecd611b13a26521926", size = 31788, upload-time = "2025-06-16T09:22:32.069Z" },
]
[[package]]
name = "django-jazzmin"
version = "3.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/84/96/21b6255e90d92a3eb4e93bea9376635d54258e0353ebb913a55e40ae9254/django_jazzmin-3.0.1.tar.gz", hash = "sha256:67ae148bade41267a09ca8e4352ddefa6121795ebbac238bb9a6564ff841eb1b", size = 2053550, upload-time = "2024-10-08T17:40:59.771Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/5b/2f8c4b168e6c41bf1e4b14d787deb23d80f618f0693db913bbe208a4a907/django_jazzmin-3.0.1-py3-none-any.whl", hash = "sha256:12a0a4c1d4fd09c2eef22acf6a1f03112b515ba695c59faa8ea80efc81c1f21b", size = 2125957, upload-time = "2024-10-08T17:40:57.359Z" },
]
[[package]]
name = "django-js-asset"
version = "3.1.2"
@ -1058,6 +1046,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/09/7a808392a751a24ffa62bec00e3085a9c1a151d728c323a5bab229ea0e58/django_timezone_field-7.1-py3-none-any.whl", hash = "sha256:93914713ed882f5bccda080eda388f7006349f25930b6122e9b07bf8db49c4b4", size = 13177, upload-time = "2025-01-11T17:49:52.142Z" },
]
[[package]]
name = "django-unfold"
version = "0.71.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ec/5b/406eae1a429b15ba04f4dfaaf53aa64fb03bcfdc6bdd0753a41027aa3daa/django_unfold-0.71.0.tar.gz", hash = "sha256:995a296f1c15f172b0d8458ff12beb420ea7e9a666fa865a60ec03f70aaf4066", size = 1101347, upload-time = "2025-11-11T16:24:03.289Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/94/ad8ba84410655e0207ffcc7c6cba0875e9f79f914e3f2e2de883f706f7c9/django_unfold-0.71.0-py3-none-any.whl", hash = "sha256:76d4019aa9052ebe2e040d868be895d8581018fdf7debca943084aa0e79c2e31", size = 1213722, upload-time = "2025-11-11T16:24:01.985Z" },
]
[[package]]
name = "django-widget-tweaks"
version = "1.5.0"
@ -1289,7 +1289,6 @@ dependencies = [
{ name = "django-extensions" },
{ name = "django-filter" },
{ name = "django-health-check" },
{ name = "django-jazzmin" },
{ name = "django-json-widget" },
{ name = "django-mailbox" },
{ name = "django-md-field" },
@ -1302,6 +1301,7 @@ dependencies = [
{ name = "django-storages" },
{ name = "django-stubs" },
{ name = "django-summernote" },
{ name = "django-unfold" },
{ name = "django-widget-tweaks" },
{ name = "djangorestframework" },
{ name = "djangorestframework-camel-case" },
@ -1394,7 +1394,6 @@ requires-dist = [
{ name = "django-extensions", specifier = "==4.1" },
{ name = "django-filter", specifier = "==25.2" },
{ name = "django-health-check", specifier = "==3.20.0" },
{ name = "django-jazzmin", specifier = "==3.0.1" },
{ name = "django-json-widget", specifier = "==2.1.0" },
{ name = "django-mailbox", specifier = "==4.10.1" },
{ name = "django-md-field", specifier = "==0.1.0" },
@ -1407,6 +1406,7 @@ requires-dist = [
{ name = "django-storages", specifier = "==1.14.6" },
{ name = "django-stubs", specifier = "==5.2.7" },
{ name = "django-summernote", specifier = "==0.8.20.0" },
{ name = "django-unfold", specifier = ">=0.71.0" },
{ name = "django-widget-tweaks", specifier = "==1.5.0" },
{ name = "djangorestframework", specifier = "==3.16.1" },
{ name = "djangorestframework-camel-case", specifier = "==1.4.2" },
@ -1444,7 +1444,7 @@ requires-dist = [
{ name = "python-slugify", specifier = "==8.0.4" },
{ name = "redis", specifier = "==7.0.1" },
{ name = "requests", specifier = "==2.32.5" },
{ name = "ruff", marker = "extra == 'linting'", specifier = "==0.14.4" },
{ name = "ruff", marker = "extra == 'linting'", specifier = "==0.14.5" },
{ name = "sentry-sdk", extras = ["django", "celery", "opentelemetry"], specifier = "==2.44.0" },
{ name = "six", specifier = "==1.17.0" },
{ name = "sphinx", marker = "extra == 'docs'", specifier = "==8.2.3" },
@ -3345,28 +3345,28 @@ wheels = [
[[package]]
name = "ruff"
version = "0.14.4"
version = "0.14.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/55/cccfca45157a2031dcbb5a462a67f7cf27f8b37d4b3b1cd7438f0f5c1df6/ruff-0.14.4.tar.gz", hash = "sha256:f459a49fe1085a749f15414ca76f61595f1a2cc8778ed7c279b6ca2e1fd19df3", size = 5587844, upload-time = "2025-11-06T22:07:45.033Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/fa/fbb67a5780ae0f704876cb8ac92d6d76da41da4dc72b7ed3565ab18f2f52/ruff-0.14.5.tar.gz", hash = "sha256:8d3b48d7d8aad423d3137af7ab6c8b1e38e4de104800f0d596990f6ada1a9fc1", size = 5615944, upload-time = "2025-11-13T19:58:51.155Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/b9/67240254166ae1eaa38dec32265e9153ac53645a6c6670ed36ad00722af8/ruff-0.14.4-py3-none-linux_armv6l.whl", hash = "sha256:e6604613ffbcf2297cd5dcba0e0ac9bd0c11dc026442dfbb614504e87c349518", size = 12606781, upload-time = "2025-11-06T22:07:01.841Z" },
{ url = "https://files.pythonhosted.org/packages/46/c8/09b3ab245d8652eafe5256ab59718641429f68681ee713ff06c5c549f156/ruff-0.14.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d99c0b52b6f0598acede45ee78288e5e9b4409d1ce7f661f0fa36d4cbeadf9a4", size = 12946765, upload-time = "2025-11-06T22:07:05.858Z" },
{ url = "https://files.pythonhosted.org/packages/14/bb/1564b000219144bf5eed2359edc94c3590dd49d510751dad26202c18a17d/ruff-0.14.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9358d490ec030f1b51d048a7fd6ead418ed0826daf6149e95e30aa67c168af33", size = 11928120, upload-time = "2025-11-06T22:07:08.023Z" },
{ url = "https://files.pythonhosted.org/packages/a3/92/d5f1770e9988cc0742fefaa351e840d9aef04ec24ae1be36f333f96d5704/ruff-0.14.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b40d27924f1f02dfa827b9c0712a13c0e4b108421665322218fc38caf615c2", size = 12370877, upload-time = "2025-11-06T22:07:10.015Z" },
{ url = "https://files.pythonhosted.org/packages/e2/29/e9282efa55f1973d109faf839a63235575519c8ad278cc87a182a366810e/ruff-0.14.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f5e649052a294fe00818650712083cddc6cc02744afaf37202c65df9ea52efa5", size = 12408538, upload-time = "2025-11-06T22:07:13.085Z" },
{ url = "https://files.pythonhosted.org/packages/8e/01/930ed6ecfce130144b32d77d8d69f5c610e6d23e6857927150adf5d7379a/ruff-0.14.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa082a8f878deeba955531f975881828fd6afd90dfa757c2b0808aadb437136e", size = 13141942, upload-time = "2025-11-06T22:07:15.386Z" },
{ url = "https://files.pythonhosted.org/packages/6a/46/a9c89b42b231a9f487233f17a89cbef9d5acd538d9488687a02ad288fa6b/ruff-0.14.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1043c6811c2419e39011890f14d0a30470f19d47d197c4858b2787dfa698f6c8", size = 14544306, upload-time = "2025-11-06T22:07:17.631Z" },
{ url = "https://files.pythonhosted.org/packages/78/96/9c6cf86491f2a6d52758b830b89b78c2ae61e8ca66b86bf5a20af73d20e6/ruff-0.14.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9f3a936ac27fb7c2a93e4f4b943a662775879ac579a433291a6f69428722649", size = 14210427, upload-time = "2025-11-06T22:07:19.832Z" },
{ url = "https://files.pythonhosted.org/packages/71/f4/0666fe7769a54f63e66404e8ff698de1dcde733e12e2fd1c9c6efb689cb5/ruff-0.14.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95643ffd209ce78bc113266b88fba3d39e0461f0cbc8b55fb92505030fb4a850", size = 13658488, upload-time = "2025-11-06T22:07:22.32Z" },
{ url = "https://files.pythonhosted.org/packages/ee/79/6ad4dda2cfd55e41ac9ed6d73ef9ab9475b1eef69f3a85957210c74ba12c/ruff-0.14.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:456daa2fa1021bc86ca857f43fe29d5d8b3f0e55e9f90c58c317c1dcc2afc7b5", size = 13354908, upload-time = "2025-11-06T22:07:24.347Z" },
{ url = "https://files.pythonhosted.org/packages/b5/60/f0b6990f740bb15c1588601d19d21bcc1bd5de4330a07222041678a8e04f/ruff-0.14.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f911bba769e4a9f51af6e70037bb72b70b45a16db5ce73e1f72aefe6f6d62132", size = 13587803, upload-time = "2025-11-06T22:07:26.327Z" },
{ url = "https://files.pythonhosted.org/packages/c9/da/eaaada586f80068728338e0ef7f29ab3e4a08a692f92eb901a4f06bbff24/ruff-0.14.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:76158a7369b3979fa878612c623a7e5430c18b2fd1c73b214945c2d06337db67", size = 12279654, upload-time = "2025-11-06T22:07:28.46Z" },
{ url = "https://files.pythonhosted.org/packages/66/d4/b1d0e82cf9bf8aed10a6d45be47b3f402730aa2c438164424783ac88c0ed/ruff-0.14.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f3b8f3b442d2b14c246e7aeca2e75915159e06a3540e2f4bed9f50d062d24469", size = 12357520, upload-time = "2025-11-06T22:07:31.468Z" },
{ url = "https://files.pythonhosted.org/packages/04/f4/53e2b42cc82804617e5c7950b7079d79996c27e99c4652131c6a1100657f/ruff-0.14.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c62da9a06779deecf4d17ed04939ae8b31b517643b26370c3be1d26f3ef7dbde", size = 12719431, upload-time = "2025-11-06T22:07:33.831Z" },
{ url = "https://files.pythonhosted.org/packages/a2/94/80e3d74ed9a72d64e94a7b7706b1c1ebaa315ef2076fd33581f6a1cd2f95/ruff-0.14.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a443a83a1506c684e98acb8cb55abaf3ef725078be40237463dae4463366349", size = 13464394, upload-time = "2025-11-06T22:07:35.905Z" },
{ url = "https://files.pythonhosted.org/packages/54/1a/a49f071f04c42345c793d22f6cf5e0920095e286119ee53a64a3a3004825/ruff-0.14.4-py3-none-win32.whl", hash = "sha256:643b69cb63cd996f1fc7229da726d07ac307eae442dd8974dbc7cf22c1e18fff", size = 12493429, upload-time = "2025-11-06T22:07:38.43Z" },
{ url = "https://files.pythonhosted.org/packages/bc/22/e58c43e641145a2b670328fb98bc384e20679b5774258b1e540207580266/ruff-0.14.4-py3-none-win_amd64.whl", hash = "sha256:26673da283b96fe35fa0c939bf8411abec47111644aa9f7cfbd3c573fb125d2c", size = 13635380, upload-time = "2025-11-06T22:07:40.496Z" },
{ url = "https://files.pythonhosted.org/packages/30/bd/4168a751ddbbf43e86544b4de8b5c3b7be8d7167a2a5cb977d274e04f0a1/ruff-0.14.4-py3-none-win_arm64.whl", hash = "sha256:dd09c292479596b0e6fec8cd95c65c3a6dc68e9ad17b8f2382130f87ff6a75bb", size = 12663065, upload-time = "2025-11-06T22:07:42.603Z" },
{ url = "https://files.pythonhosted.org/packages/68/31/c07e9c535248d10836a94e4f4e8c5a31a1beed6f169b31405b227872d4f4/ruff-0.14.5-py3-none-linux_armv6l.whl", hash = "sha256:f3b8248123b586de44a8018bcc9fefe31d23dda57a34e6f0e1e53bd51fd63594", size = 13171630, upload-time = "2025-11-13T19:57:54.894Z" },
{ url = "https://files.pythonhosted.org/packages/8e/5c/283c62516dca697cd604c2796d1487396b7a436b2f0ecc3fd412aca470e0/ruff-0.14.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f7a75236570318c7a30edd7f5491945f0169de738d945ca8784500b517163a72", size = 13413925, upload-time = "2025-11-13T19:57:59.181Z" },
{ url = "https://files.pythonhosted.org/packages/b6/f3/aa319f4afc22cb6fcba2b9cdfc0f03bbf747e59ab7a8c5e90173857a1361/ruff-0.14.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d146132d1ee115f8802356a2dc9a634dbf58184c51bff21f313e8cd1c74899a", size = 12574040, upload-time = "2025-11-13T19:58:02.056Z" },
{ url = "https://files.pythonhosted.org/packages/f9/7f/cb5845fcc7c7e88ed57f58670189fc2ff517fe2134c3821e77e29fd3b0c8/ruff-0.14.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2380596653dcd20b057794d55681571a257a42327da8894b93bbd6111aa801f", size = 13009755, upload-time = "2025-11-13T19:58:05.172Z" },
{ url = "https://files.pythonhosted.org/packages/21/d2/bcbedbb6bcb9253085981730687ddc0cc7b2e18e8dc13cf4453de905d7a0/ruff-0.14.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68", size = 12937641, upload-time = "2025-11-13T19:58:08.345Z" },
{ url = "https://files.pythonhosted.org/packages/a4/58/e25de28a572bdd60ffc6bb71fc7fd25a94ec6a076942e372437649cbb02a/ruff-0.14.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88f0770d42b7fa02bbefddde15d235ca3aa24e2f0137388cc15b2dcbb1f7c7a7", size = 13610854, upload-time = "2025-11-13T19:58:11.419Z" },
{ url = "https://files.pythonhosted.org/packages/7d/24/43bb3fd23ecee9861970978ea1a7a63e12a204d319248a7e8af539984280/ruff-0.14.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78", size = 15061088, upload-time = "2025-11-13T19:58:14.551Z" },
{ url = "https://files.pythonhosted.org/packages/23/44/a022f288d61c2f8c8645b24c364b719aee293ffc7d633a2ca4d116b9c716/ruff-0.14.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b595bedf6bc9cab647c4a173a61acf4f1ac5f2b545203ba82f30fcb10b0318fb", size = 14734717, upload-time = "2025-11-13T19:58:17.518Z" },
{ url = "https://files.pythonhosted.org/packages/58/81/5c6ba44de7e44c91f68073e0658109d8373b0590940efe5bd7753a2585a3/ruff-0.14.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f55382725ad0bdb2e8ee2babcbbfb16f124f5a59496a2f6a46f1d9d99d93e6e2", size = 14028812, upload-time = "2025-11-13T19:58:20.533Z" },
{ url = "https://files.pythonhosted.org/packages/ad/ef/41a8b60f8462cb320f68615b00299ebb12660097c952c600c762078420f8/ruff-0.14.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7497d19dce23976bdaca24345ae131a1d38dcfe1b0850ad8e9e6e4fa321a6e19", size = 13825656, upload-time = "2025-11-13T19:58:23.345Z" },
{ url = "https://files.pythonhosted.org/packages/7c/00/207e5de737fdb59b39eb1fac806904fe05681981b46d6a6db9468501062e/ruff-0.14.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:410e781f1122d6be4f446981dd479470af86537fb0b8857f27a6e872f65a38e4", size = 13959922, upload-time = "2025-11-13T19:58:26.537Z" },
{ url = "https://files.pythonhosted.org/packages/bc/7e/fa1f5c2776db4be405040293618846a2dece5c70b050874c2d1f10f24776/ruff-0.14.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01be527ef4c91a6d55e53b337bfe2c0f82af024cc1a33c44792d6844e2331e1", size = 12932501, upload-time = "2025-11-13T19:58:29.822Z" },
{ url = "https://files.pythonhosted.org/packages/67/d8/d86bf784d693a764b59479a6bbdc9515ae42c340a5dc5ab1dabef847bfaa/ruff-0.14.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f66e9bb762e68d66e48550b59c74314168ebb46199886c5c5aa0b0fbcc81b151", size = 12927319, upload-time = "2025-11-13T19:58:32.923Z" },
{ url = "https://files.pythonhosted.org/packages/ac/de/ee0b304d450ae007ce0cb3e455fe24fbcaaedae4ebaad6c23831c6663651/ruff-0.14.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d93be8f1fa01022337f1f8f3bcaa7ffee2d0b03f00922c45c2207954f351f465", size = 13206209, upload-time = "2025-11-13T19:58:35.952Z" },
{ url = "https://files.pythonhosted.org/packages/33/aa/193ca7e3a92d74f17d9d5771a765965d2cf42c86e6f0fd95b13969115723/ruff-0.14.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c135d4b681f7401fe0e7312017e41aba9b3160861105726b76cfa14bc25aa367", size = 13953709, upload-time = "2025-11-13T19:58:39.002Z" },
{ url = "https://files.pythonhosted.org/packages/cc/f1/7119e42aa1d3bf036ffc9478885c2e248812b7de9abea4eae89163d2929d/ruff-0.14.5-py3-none-win32.whl", hash = "sha256:c83642e6fccfb6dea8b785eb9f456800dcd6a63f362238af5fc0c83d027dd08b", size = 12925808, upload-time = "2025-11-13T19:58:42.779Z" },
{ url = "https://files.pythonhosted.org/packages/3b/9d/7c0a255d21e0912114784e4a96bf62af0618e2190cae468cd82b13625ad2/ruff-0.14.5-py3-none-win_amd64.whl", hash = "sha256:9d55d7af7166f143c94eae1db3312f9ea8f95a4defef1979ed516dbb38c27621", size = 14331546, upload-time = "2025-11-13T19:58:45.691Z" },
{ url = "https://files.pythonhosted.org/packages/e5/80/69756670caedcf3b9be597a6e12276a6cf6197076eb62aad0c608f8efce0/ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4", size = 13433331, upload-time = "2025-11-13T19:58:48.434Z" },
]
[[package]]