From 376c73ba26fc6d3dd03ad31d48fcd6c517dc8f72 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Sat, 15 Nov 2025 02:29:23 +0300 Subject: [PATCH] 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. --- engine/blog/admin.py | 6 +- engine/core/admin.py | 31 ++++----- engine/payments/admin.py | 8 +-- engine/vibes_auth/admin.py | 26 ++++---- evibes/settings/base.py | 6 ++ evibes/settings/unfold.py | 128 +++++++++++++++++++++++++++++++------ 6 files changed, 148 insertions(+), 57 deletions(-) diff --git a/engine/blog/admin.py b/engine/blog/admin.py index 611edfe2..c5117a14 100644 --- a/engine/blog/admin.py +++ b/engine/blog/admin.py @@ -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] diff --git a/engine/core/admin.py b/engine/core/admin.py index 900a7bb2..fba4a8ec 100644 --- a/engine/core/admin.py +++ b/engine/core/admin.py @@ -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) diff --git a/engine/payments/admin.py b/engine/payments/admin.py index 78d54b63..4cd4b905 100644 --- a/engine/payments/admin.py +++ b/engine/payments/admin.py @@ -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 diff --git a/engine/vibes_auth/admin.py b/engine/vibes_auth/admin.py index f761beed..307c7cf4 100644 --- a/engine/vibes_auth/admin.py +++ b/engine/vibes_auth/admin.py @@ -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 diff --git a/evibes/settings/base.py b/evibes/settings/base.py index aae74e7f..298a8bf5 100644 --- a/evibes/settings/base.py +++ b/evibes/settings/base.py @@ -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", diff --git a/evibes/settings/unfold.py b/evibes/settings/unfold.py index 4fe4feaa..0e3bca04 100644 --- a/evibes/settings/unfold.py +++ b/evibes/settings/unfold.py @@ -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"), + }, + ], + }, + ], }