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 unfold.admin import ModelAdmin
from engine.blog.models import Post, PostTag
from engine.core.admin import ActivationActionsMixin, FieldsetsMixin
from .models import Post, PostTag
@register(Post)
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 django.apps import AppConfig, apps
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.messages import constants as messages
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.utils import get_translation_fields
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.models import (
@ -65,15 +67,15 @@ class FieldsetsMixin:
for orig in transoptions.local_fields:
translation_fields += get_translation_fields(orig)
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
if self.general_fields:
fieldsets.append((_("general"), {"fields": self.general_fields}))
fieldsets.append((_("general"), {"classes": ["tab"], "fields": self.general_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:
fieldsets.append((_("additional info"), {"fields": self.additional_fields}))
fieldsets.append((_("additional info"), {"classes": ["tab"], "fields": self.additional_fields}))
opts = self.model._meta
meta_fields = []
@ -91,14 +93,14 @@ class FieldsetsMixin:
meta_fields.append("human_readable_id")
if meta_fields:
fieldsets.append((_("metadata"), {"fields": meta_fields}))
fieldsets.append((_("metadata"), {"classes": ["tab"], "fields": meta_fields}))
ts = []
for name in ("created", "modified"):
if any(f.name == name for f in opts.fields):
ts.append(name)
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]
return fieldsets # type: ignore [return-value]
@ -140,10 +142,9 @@ class AttributeValueInline(TabularInline): # type: ignore [type-arg]
model = AttributeValue
extra = 0
autocomplete_fields = ["attribute"]
is_navtab = True
verbose_name = _("attribute value")
verbose_name_plural = _("attribute values")
icon = "fa-solid fa-list-ul"
tab = True
def get_queryset(self, request):
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]
model = ProductImage
extra = 0
is_navtab = True
tab = True
verbose_name = _("image")
verbose_name_plural = _("images")
icon = "fa-regular fa-images"
def get_queryset(self, request):
return super().get_queryset(request).select_related("product")
@ -165,10 +165,9 @@ class StockInline(TabularInline): # type: ignore [type-arg]
model = Stock
extra = 0
form = StockForm
is_navtab = True
tab = True
verbose_name = _("stock")
verbose_name_plural = _("stocks")
icon = "fa-solid fa-boxes-stacked"
def get_queryset(self, request):
return super().get_queryset(request).select_related("vendor", "product")
@ -179,10 +178,9 @@ class OrderProductInline(TabularInline): # type: ignore [type-arg]
extra = 0
readonly_fields = ("product", "quantity", "buy_price")
form = OrderProductForm
is_navtab = True
verbose_name = _("order product")
verbose_name_plural = _("order products")
icon = "fa-solid fa-boxes-packing"
tab = True
def get_queryset(self, request):
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"
extra = 0
fields = ("name", "description", "is_active", "image", "markup_percent")
is_navtab = True
tab = True
verbose_name = _("children")
verbose_name_plural = _("children")
icon = "fa-solid fa-leaf"
@register(AttributeGroup)

View file

@ -1,15 +1,15 @@
from django.contrib import admin
from django.contrib.admin import ModelAdmin, register
from django.contrib.admin import register
from django.db.models import QuerySet
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin, TabularInline
from engine.core.admin import ActivationActionsMixin
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
form = TransactionForm
extra = 1

View file

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

View file

@ -10,6 +10,10 @@ EVIBES_VERSION = "2025.4"
RELEASE_DATE = datetime(2025, 11, 9)
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
INITIALIZED: bool = (BASE_DIR / ".initialized").exists()
@ -108,6 +112,8 @@ INSTALLED_APPS: list[str] = [
"unfold",
"unfold.contrib.filters",
"unfold.contrib.forms",
"unfold.contrib.inlines",
"unfold.contrib.constance",
"modeltranslation",
"django.contrib.admin",
"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.
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, SUPPORT_CONTACT, TASKBOARD_URL
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_TITLE": f"{PROJECT_NAME} Dashboard",
"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.
"SHOW_LANGUAGES": True,
"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"),
},
],
},
],
}