Features: 1) Add tab support for inline admin classes; 2) Introduce new settings for taskboard URL and support contact;

Fixes: 1) Remove redundant imports from admin.py;

Extra: 1) Update inline classes to inherit from TabularInline; 2) Add unfold.contrib modules to INSTALLED_APPS; 3) Reorder imports in admin.py for consistency.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-11-15 02:29:23 +03:00
parent 43dc556063
commit 376c73ba26
6 changed files with 148 additions and 57 deletions

View file

@ -1,10 +1,10 @@
from django.contrib.admin import ModelAdmin, register from django.contrib.admin import register
from django_summernote.admin import SummernoteModelAdminMixin from django_summernote.admin import SummernoteModelAdminMixin
from unfold.admin import ModelAdmin
from engine.blog.models import Post, PostTag
from engine.core.admin import ActivationActionsMixin, FieldsetsMixin from engine.core.admin import ActivationActionsMixin, FieldsetsMixin
from .models import Post, PostTag
@register(Post) @register(Post)
class PostAdmin(SummernoteModelAdminMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg] class PostAdmin(SummernoteModelAdminMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]

View file

@ -5,7 +5,7 @@ from constance.admin import Config
from constance.admin import ConstanceAdmin as BaseConstanceAdmin from constance.admin import ConstanceAdmin as BaseConstanceAdmin
from django.apps import AppConfig, apps from django.apps import AppConfig, apps
from django.conf import settings from django.conf import settings
from django.contrib.admin import ModelAdmin, TabularInline, action, register, site from django.contrib.admin import register, site
from django.contrib.gis.admin import GISModelAdmin from django.contrib.gis.admin import GISModelAdmin
from django.contrib.messages import constants as messages from django.contrib.messages import constants as messages
from django.db.models import Model from django.db.models import Model
@ -15,6 +15,8 @@ from django.utils.translation import gettext_lazy as _
from modeltranslation.translator import NotRegistered, translator from modeltranslation.translator import NotRegistered, translator
from modeltranslation.utils import get_translation_fields from modeltranslation.utils import get_translation_fields
from mptt.admin import DraggableMPTTAdmin from mptt.admin import DraggableMPTTAdmin
from unfold.admin import ModelAdmin, TabularInline
from unfold.decorators import action
from engine.core.forms import CRMForm, OrderForm, OrderProductForm, StockForm, VendorForm from engine.core.forms import CRMForm, OrderForm, OrderProductForm, StockForm, VendorForm
from engine.core.models import ( from engine.core.models import (
@ -65,15 +67,15 @@ class FieldsetsMixin:
for orig in transoptions.local_fields: for orig in transoptions.local_fields:
translation_fields += get_translation_fields(orig) translation_fields += get_translation_fields(orig)
if translation_fields: if translation_fields:
fss = list(fss) + [(_("translations"), {"fields": translation_fields})] # type: ignore [list-item] fss = list(fss) + [(_("translations"), {"classes": ["tab"], "fields": translation_fields})] # type: ignore [list-item]
return fss return fss
if self.general_fields: if self.general_fields:
fieldsets.append((_("general"), {"fields": self.general_fields})) fieldsets.append((_("general"), {"classes": ["tab"], "fields": self.general_fields}))
if self.relation_fields: if self.relation_fields:
fieldsets.append((_("relations"), {"fields": self.relation_fields})) fieldsets.append((_("relations"), {"classes": ["tab"], "fields": self.relation_fields}))
if self.additional_fields: if self.additional_fields:
fieldsets.append((_("additional info"), {"fields": self.additional_fields})) fieldsets.append((_("additional info"), {"classes": ["tab"], "fields": self.additional_fields}))
opts = self.model._meta opts = self.model._meta
meta_fields = [] meta_fields = []
@ -91,14 +93,14 @@ class FieldsetsMixin:
meta_fields.append("human_readable_id") meta_fields.append("human_readable_id")
if meta_fields: if meta_fields:
fieldsets.append((_("metadata"), {"fields": meta_fields})) fieldsets.append((_("metadata"), {"classes": ["tab"], "fields": meta_fields}))
ts = [] ts = []
for name in ("created", "modified"): for name in ("created", "modified"):
if any(f.name == name for f in opts.fields): if any(f.name == name for f in opts.fields):
ts.append(name) ts.append(name)
if ts: if ts:
fieldsets.append((_("timestamps"), {"fields": ts, "classes": ["collapse"]})) fieldsets.append((_("timestamps"), {"classes": ["tab"], "fields": ts}))
fieldsets = add_translations_fieldset(fieldsets) # type: ignore [arg-type, assignment] fieldsets = add_translations_fieldset(fieldsets) # type: ignore [arg-type, assignment]
return fieldsets # type: ignore [return-value] return fieldsets # type: ignore [return-value]
@ -140,10 +142,9 @@ class AttributeValueInline(TabularInline): # type: ignore [type-arg]
model = AttributeValue model = AttributeValue
extra = 0 extra = 0
autocomplete_fields = ["attribute"] autocomplete_fields = ["attribute"]
is_navtab = True
verbose_name = _("attribute value") verbose_name = _("attribute value")
verbose_name_plural = _("attribute values") verbose_name_plural = _("attribute values")
icon = "fa-solid fa-list-ul" tab = True
def get_queryset(self, request): def get_queryset(self, request):
return super().get_queryset(request).select_related("attribute", "product") return super().get_queryset(request).select_related("attribute", "product")
@ -152,10 +153,9 @@ class AttributeValueInline(TabularInline): # type: ignore [type-arg]
class ProductImageInline(TabularInline): # type: ignore [type-arg] class ProductImageInline(TabularInline): # type: ignore [type-arg]
model = ProductImage model = ProductImage
extra = 0 extra = 0
is_navtab = True tab = True
verbose_name = _("image") verbose_name = _("image")
verbose_name_plural = _("images") verbose_name_plural = _("images")
icon = "fa-regular fa-images"
def get_queryset(self, request): def get_queryset(self, request):
return super().get_queryset(request).select_related("product") return super().get_queryset(request).select_related("product")
@ -165,10 +165,9 @@ class StockInline(TabularInline): # type: ignore [type-arg]
model = Stock model = Stock
extra = 0 extra = 0
form = StockForm form = StockForm
is_navtab = True tab = True
verbose_name = _("stock") verbose_name = _("stock")
verbose_name_plural = _("stocks") verbose_name_plural = _("stocks")
icon = "fa-solid fa-boxes-stacked"
def get_queryset(self, request): def get_queryset(self, request):
return super().get_queryset(request).select_related("vendor", "product") return super().get_queryset(request).select_related("vendor", "product")
@ -179,10 +178,9 @@ class OrderProductInline(TabularInline): # type: ignore [type-arg]
extra = 0 extra = 0
readonly_fields = ("product", "quantity", "buy_price") readonly_fields = ("product", "quantity", "buy_price")
form = OrderProductForm form = OrderProductForm
is_navtab = True
verbose_name = _("order product") verbose_name = _("order product")
verbose_name_plural = _("order products") verbose_name_plural = _("order products")
icon = "fa-solid fa-boxes-packing" tab = True
def get_queryset(self, request): def get_queryset(self, request):
return super().get_queryset(request).select_related("product").only("product__name") return super().get_queryset(request).select_related("product").only("product__name")
@ -193,10 +191,9 @@ class CategoryChildrenInline(TabularInline): # type: ignore [type-arg]
fk_name = "parent" fk_name = "parent"
extra = 0 extra = 0
fields = ("name", "description", "is_active", "image", "markup_percent") fields = ("name", "description", "is_active", "image", "markup_percent")
is_navtab = True tab = True
verbose_name = _("children") verbose_name = _("children")
verbose_name_plural = _("children") verbose_name_plural = _("children")
icon = "fa-solid fa-leaf"
@register(AttributeGroup) @register(AttributeGroup)

View file

@ -1,15 +1,15 @@
from django.contrib import admin from django.contrib.admin import register
from django.contrib.admin import ModelAdmin, register
from django.db.models import QuerySet from django.db.models import QuerySet
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin, TabularInline
from engine.core.admin import ActivationActionsMixin from engine.core.admin import ActivationActionsMixin
from engine.payments.forms import GatewayForm, TransactionForm from engine.payments.forms import GatewayForm, TransactionForm
from engine.payments.models import Balance, Transaction, Gateway from engine.payments.models import Balance, Gateway, Transaction
class TransactionInline(admin.TabularInline): # type: ignore [type-arg] class TransactionInline(TabularInline): # type: ignore [type-arg]
model = Transaction model = Transaction
form = TransactionForm form = TransactionForm
extra = 1 extra = 1

View file

@ -1,6 +1,7 @@
from typing import Any from typing import Any
from django.contrib import admin from django.contrib import admin
from django.contrib.admin import register
from django.contrib.auth.admin import ( from django.contrib.auth.admin import (
GroupAdmin as BaseGroupAdmin, GroupAdmin as BaseGroupAdmin,
) )
@ -25,6 +26,7 @@ from rest_framework_simplejwt.token_blacklist.models import (
from rest_framework_simplejwt.token_blacklist.models import ( from rest_framework_simplejwt.token_blacklist.models import (
OutstandingToken as BaseOutstandingToken, OutstandingToken as BaseOutstandingToken,
) )
from unfold.admin import ModelAdmin, TabularInline
from engine.core.admin import ActivationActionsMixin from engine.core.admin import ActivationActionsMixin
from engine.core.models import Order from engine.core.models import Order
@ -32,16 +34,16 @@ from engine.payments.models import Balance
from engine.vibes_auth.forms import UserForm from engine.vibes_auth.forms import UserForm
from engine.vibes_auth.models import ( from engine.vibes_auth.models import (
BlacklistedToken, BlacklistedToken,
Group,
OutstandingToken,
User,
ChatMessage, ChatMessage,
ChatThread, ChatThread,
Group,
OutstandingToken,
ThreadStatus, ThreadStatus,
User,
) )
class BalanceInline(admin.TabularInline): # type: ignore [type-arg] class BalanceInline(TabularInline): # type: ignore [type-arg]
model = Balance model = Balance
can_delete = False can_delete = False
extra = 0 extra = 0
@ -51,7 +53,7 @@ class BalanceInline(admin.TabularInline): # type: ignore [type-arg]
icon = "fa-solid fa-wallet" icon = "fa-solid fa-wallet"
class OrderInline(admin.TabularInline): # type: ignore [type-arg] class OrderInline(TabularInline): # type: ignore [type-arg]
model = Order model = Order
extra = 0 extra = 0
verbose_name = _("order") verbose_name = _("order")
@ -60,7 +62,7 @@ class OrderInline(admin.TabularInline): # type: ignore [type-arg]
icon = "fa-solid fa-cart-shopping" icon = "fa-solid fa-cart-shopping"
class UserAdmin(ActivationActionsMixin, BaseUserAdmin): # type: ignore [misc, type-arg] class UserAdmin(ActivationActionsMixin, BaseUserAdmin, ModelAdmin): # type: ignore [misc, type-arg]
inlines = (BalanceInline, OrderInline) inlines = (BalanceInline, OrderInline)
fieldsets = ( fieldsets = (
(None, {"fields": ("email", "password")}), (None, {"fields": ("email", "password")}),
@ -125,8 +127,8 @@ class UserAdmin(ActivationActionsMixin, BaseUserAdmin): # type: ignore [misc, t
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@admin.register(ChatThread) @register(ChatThread)
class ChatThreadAdmin(admin.ModelAdmin): class ChatThreadAdmin(ModelAdmin):
list_display = ( list_display = (
"uuid", "uuid",
"user", "user",
@ -161,7 +163,7 @@ class ChatThreadAdmin(admin.ModelAdmin):
queryset.update(status=ThreadStatus.OPEN) queryset.update(status=ThreadStatus.OPEN)
@admin.register(ChatMessage) @register(ChatMessage)
class ChatMessageAdmin(admin.ModelAdmin): class ChatMessageAdmin(admin.ModelAdmin):
list_display = ("uuid", "thread", "sender_type", "sender_user", "sent_at") list_display = ("uuid", "thread", "sender_type", "sender_user", "sent_at")
list_filter = ("sender_type",) list_filter = ("sender_type",)
@ -170,15 +172,15 @@ class ChatMessageAdmin(admin.ModelAdmin):
readonly_fields = ("created", "modified") readonly_fields = ("created", "modified")
class GroupAdmin(BaseGroupAdmin): class GroupAdmin(BaseGroupAdmin, ModelAdmin):
pass pass
class BlacklistedTokenAdmin(BaseBlacklistedTokenAdmin): class BlacklistedTokenAdmin(BaseBlacklistedTokenAdmin, ModelAdmin):
pass pass
class OutstandingTokenAdmin(BaseOutstandingTokenAdmin): class OutstandingTokenAdmin(BaseOutstandingTokenAdmin, ModelAdmin):
pass pass

View file

@ -10,6 +10,10 @@ EVIBES_VERSION = "2025.4"
RELEASE_DATE = datetime(2025, 11, 9) RELEASE_DATE = datetime(2025, 11, 9)
PROJECT_NAME = getenv("EVIBES_PROJECT_NAME", "eVibes") PROJECT_NAME = getenv("EVIBES_PROJECT_NAME", "eVibes")
TASKBOARD_URL = getenv(
"EVIBES_TASKBOARD_URL", "https://plane.wiseless.xyz/spaces/issues/dd33cb0ab9b04ef08a10f7eefae6d90c/?board=kanban"
)
SUPPORT_CONTACT = getenv("EVIBES_SUPPORT_CONTACT", "https://t.me/fureunoir")
BASE_DIR: Path = Path(__file__).resolve().parent.parent.parent BASE_DIR: Path = Path(__file__).resolve().parent.parent.parent
INITIALIZED: bool = (BASE_DIR / ".initialized").exists() INITIALIZED: bool = (BASE_DIR / ".initialized").exists()
@ -108,6 +112,8 @@ INSTALLED_APPS: list[str] = [
"unfold", "unfold",
"unfold.contrib.filters", "unfold.contrib.filters",
"unfold.contrib.forms", "unfold.contrib.forms",
"unfold.contrib.inlines",
"unfold.contrib.constance",
"modeltranslation", "modeltranslation",
"django.contrib.admin", "django.contrib.admin",
"django.contrib.admindocs", "django.contrib.admindocs",

View file

@ -1,28 +1,114 @@
"""django-unfold configuration. from django.templatetags.static import static
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
This module defines branding for the Django admin using django-unfold. from evibes.settings.base import PROJECT_NAME, SUPPORT_CONTACT, TASKBOARD_URL
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 = { UNFOLD = {
# Text shown in the browser title bar and in the admin header "SITE_TITLE": f"{PROJECT_NAME} Dashboard",
"SITE_TITLE": f"{PROJECT_NAME} Admin",
"SITE_HEADER": PROJECT_NAME, "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", "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", "SITE_ICON": "favicon.ico",
# Sidebar behavior, search etc. (keep defaults minimal) "SHOW_LANGUAGES": True,
# Unfold automatically respects user OS light/dark theme; no forcing here. "LOGIN": {
"image": lambda request: static("logo.png"),
},
"COMMAND": {
"search_models": True,
"show_history": True,
},
"EXTENSIONS": {
"modeltranslation": {
"flags": {
"ar-ar": "🇸🇦",
"cs-cz": "🇨🇿",
"da-dk": "🇩🇰",
"de-de": "🇩🇪",
"en-gb": "🇬🇧",
"en-us": "🇺🇸",
"es-es": "🇪🇸",
"fa-ir": "🇮🇷",
"fr-fr": "🇫🇷",
"he-il": "🇮🇱",
"hi-in": "🇮🇳",
"hr-hr": "🇭🇷",
"id-id": "🇮🇩",
"it-it": "🇮🇹",
"ja-jp": "🇯🇵",
"kk-kz": "🇰🇿",
"ko-kr": "🇰🇷",
"nl-nl": "🇳🇱",
"no-no": "🇳🇴",
"pl-pl": "🇵🇱",
"pt-br": "🇧🇷",
"ro-ro": "🇷🇴",
"ru-ru": "🇷🇺",
"sv-se": "🇸🇪",
"th-th": "🇹🇭",
"tr-tr": "🇹🇷",
"vi-vn": "🇻🇳",
"zh-hans": "🇨🇳",
},
},
},
"SIDEBAR": {
"show_search": True,
"navigation": [
{
"title": _("Menu"),
"separator": True,
"collapsible": True,
"items": [
{
"title": _("Dashboard"),
"icon": "dashboard",
"link": reverse_lazy("admin:index"),
},
{
"title": _("Health"),
"icon": "health_metrics",
"link": reverse_lazy("health_check:health_check_home"),
},
{
"title": _("Swagger"),
"icon": "integration_instructions",
"link": reverse_lazy("swagger-ui-platform"),
},
{
"title": _("Redoc"),
"icon": "integration_instructions",
"link": reverse_lazy("redoc-ui-platform"),
},
{
"title": _("GraphQL"),
"icon": "graph_5",
"link": reverse_lazy("graphql-platform"),
},
{
"title": _("Taskboard"),
"icon": "view_kanban",
"link": TASKBOARD_URL,
},
{
"title": _("Support"),
"icon": "contact_support",
"link": SUPPORT_CONTACT,
},
],
},
],
},
"TABS": [
{
"models": [
"core.product",
],
"items": [
{
"title": _("Your custom title"),
"link": reverse_lazy("admin:core_product_changelist"),
},
],
},
],
} }