Features: 1) Replace I18NFieldsetMixin with TranslationFieldsetMixin across all applicable admin classes; 2) Add new inlines for managing related objects such as OrderProduct and CategoryChildren; 3) Introduce support for dynamic translation fields in admin fieldsets.

Fixes: 1) Correct `notifications` field handling in `OrderAdmin` and `OrderItemAdmin` save logic; 2) Fix admin `get_queryset` prefetching logic for various models.

Extra: Remove outdated labels and sections in fieldsets; clean up unused imports and commented code for readability.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-06-22 16:15:48 +03:00
parent 5425225e31
commit c682137fc5
2 changed files with 139 additions and 285 deletions

View file

@ -15,7 +15,6 @@ class NiceModel(Model):
default=uuid.uuid4,
editable=False,
)
uuid.id_for_label = "uuid"
is_active: bool = BooleanField( # type: ignore
default=True,
verbose_name=_("is active"),
@ -23,15 +22,12 @@ class NiceModel(Model):
"if set to false, this object can't be seen by users without needed permission"
),
)
is_active.id_for_label = "is_active"
created: datetime = CreationDateTimeField( # type: ignore
_("created"), help_text=_("when the object first appeared on the database")
)
created.id_for_label = "created"
modified: datetime = ModificationDateTimeField( # type: ignore
_("modified"), help_text=_("when the object was last modified")
)
modified.id_for_label = "modified"
def save(self, **kwargs):
self.update_modified = kwargs.pop(

View file

@ -4,11 +4,8 @@ from constance.admin import ConstanceAdmin as BaseConstanceAdmin
from django.apps import apps
from django.contrib import admin
from django.contrib.admin import ModelAdmin, TabularInline
from django.contrib.admin.options import InlineModelAdmin
from django.contrib.gis.admin import GISModelAdmin
from django.db.models import Model
from django.forms import modelform_factory
from django.urls import path
from django.utils.translation import gettext_lazy as _
from modeltranslation.translator import translator
from modeltranslation.utils import get_translation_fields
@ -38,72 +35,37 @@ from .models import (
Wishlist,
)
SECTION_GENERAL = _("general")
SECTION_I18N = _("I18N")
SECTION_META = _("metadata")
SECTION_DATES = _("timestamps")
SECTION_RELATIONS = _("relations")
class TranslationFieldsetMixin:
model: Model
class I18NFieldsetMixin:
model: type[Model]
def get_inline_instances(self, request, obj=None):
inlines = super().get_inline_instances(request, obj)
trans_opts = translator.get_options_for_model(self.model)
def get_fieldsets(self, request, obj=None):
fieldsets = super().get_fieldsets(request, obj)
opts = translator.get_options_for_model(self.model)
translation_fields = []
for orig in trans_opts.local_fields:
for orig in opts.local_fields:
translation_fields += get_translation_fields(orig)
class _TranslationInline(InlineModelAdmin[self.model, self.model]):
model = self.model
form = modelform_factory(
self.model,
fields=translation_fields,
formfield_callback=lambda f, **kw: super(
I18NFieldsetMixin, self
).formfield_for_dbfield(f, request, **kw),
if translation_fields:
fieldsets = list(fieldsets)
fieldsets.append(
(
_("translations"),
{"fields": translation_fields},
)
template = "admin/edit_inline/tabular.html"
is_navtab = True
verbose_name = _("translations")
verbose_name_plural = _("translations")
extra = 0
icon = "fa-solid fa-language"
def get_queryset(self, request):
return (
self.model.objects.filter(pk=obj.pk)
if obj
else self.model.objects.none()
)
def get_formset(self, request, obj=None, **kw):
return super().get_formset(
request,
obj,
**{
**kw,
"form": self.form,
"fields": translation_fields,
"exclude": [],
},
)
inlines.append(_TranslationInline(self.model, self.admin_site))
return inlines
return fieldsets
class BasicModelAdmin(ModelAdmin):
@admin.action(description=str(_("activate selected %(verbose_name_plural)s")))
def activate_selected(self, _request, queryset) -> str:
queryset.update(is_active=True)
return ""
return str(_("%(verbose_name_plural)s activated successfully!"))
@admin.action(description=str(_("deactivate selected %(verbose_name_plural)s")))
def deactivate_selected(self, _request, queryset) -> str:
queryset.update(is_active=False)
return ""
return str(_("%(verbose_name_plural)s deactivated successfully."))
def get_actions(self, request):
actions = super().get_actions(request)
@ -130,134 +92,13 @@ class AttributeValueInline(TabularInline):
icon = "fa-regular fa-circle-dot"
@admin.register(AttributeGroup)
class AttributeGroupAdmin(BasicModelAdmin, I18NFieldsetMixin):
list_display = ("name", "modified")
search_fields = (
"uuid",
"name",
)
fieldsets = (
(
SECTION_GENERAL,
{
"fields": ("name", "parent", "is_active"),
},
),
(
SECTION_META,
{
"fields": ("uuid",),
},
),
)
@admin.register(Attribute)
class AttributeAdmin(BasicModelAdmin, I18NFieldsetMixin):
list_display = ("name", "group", "value_type", "modified")
list_filter = ("value_type", "group", "is_active")
search_fields = ("uuid", "name", "group__name")
autocomplete_fields = ["categories", "group"]
@admin.register(AttributeValue)
class AttributeValueAdmin(BasicModelAdmin, I18NFieldsetMixin):
list_display = ("attribute", "value", "modified")
list_filter = ("attribute__group", "is_active")
search_fields = ("uuid", "value", "attribute__name")
autocomplete_fields = ["attribute"]
class CategoryChildrenInline(admin.TabularInline):
model = Category
fk_name = "parent"
extra = 0
fields = ("name", "description", "is_active", "image", "markup_percent")
icon = "fa-regular fa-circle-dot"
@admin.register(Category)
class CategoryAdmin(DraggableMPTTAdmin, BasicModelAdmin, I18NFieldsetMixin):
mptt_indent_field = "name"
list_display = ("indented_title", "parent", "is_active", "modified")
# noinspection PyUnresolvedReferences
list_filter = ("is_active", "level", "created", "modified")
list_display_links = ("indented_title",)
search_fields = (
"uuid",
"name",
)
inlines = [CategoryChildrenInline]
fieldsets = (
(
SECTION_GENERAL,
{
"fields": (
"name",
"slug",
"parent",
"is_active",
),
},
),
(
SECTION_RELATIONS,
{
"fields": ("tags",),
},
),
(
SECTION_META,
{
"fields": ("uuid",),
},
),
(
SECTION_DATES,
{
"fields": ("created", "modified"),
},
),
)
autocomplete_fields = ["parent", "tags"]
readonly_fields = (
"uuid",
"slug",
"created",
"modified",
)
def indented_title(self, instance):
return instance.name
indented_title.short_description = _("name") # type: ignore
indented_title.admin_order_field = "name" # type: ignore
@admin.register(Brand)
class BrandAdmin(BasicModelAdmin, I18NFieldsetMixin):
list_display = ("name",)
list_filter = ("categories", "is_active")
search_fields = (
"uuid",
"name",
"categories__name",
)
readonly_fields = (
"uuid",
"slug",
)
class ProductImageInline(TabularInline):
model = ProductImage
extra = 0
is_navtab = True
verbose_name = _("image")
verbose_name_plural = _("images")
icon = "fa-regular fa-circle-dot"
icon = "fa-regular fa-images"
class StockInline(TabularInline):
@ -266,11 +107,87 @@ class StockInline(TabularInline):
is_navtab = True
verbose_name = _("stock")
verbose_name_plural = _("stocks")
icon = "fa-regular fa-boxes-stacked"
class OrderProductInline(TabularInline):
model = OrderProduct
extra = 0
readonly_fields = ("product", "quantity", "buy_price")
form = OrderProductForm
is_navtab = True
verbose_name = _("order product")
verbose_name_plural = _("order products")
icon = "fa-regular fa-circle-dot"
def get_queryset(self, request):
return (
super()
.get_queryset(request)
.select_related("product")
.only("product__name")
)
class CategoryChildrenInline(TabularInline):
model = Category
fk_name = "parent"
extra = 0
fields = ("name", "description", "is_active", "image", "markup_percent")
icon = "fa-regular fa-circle-dot"
# Admin registrations
@admin.register(AttributeGroup)
class AttributeGroupAdmin(TranslationFieldsetMixin, BasicModelAdmin):
list_display = ("name", "modified")
search_fields = ("uuid", "name")
@admin.register(Attribute)
class AttributeAdmin(TranslationFieldsetMixin, BasicModelAdmin):
list_display = ("name", "group", "value_type", "modified")
list_filter = ("value_type", "group", "is_active")
search_fields = ("uuid", "name", "group__name")
autocomplete_fields = ["categories", "group"]
@admin.register(AttributeValue)
class AttributeValueAdmin(TranslationFieldsetMixin, BasicModelAdmin):
list_display = ("attribute", "value", "modified")
list_filter = ("attribute__group", "is_active")
search_fields = ("uuid", "value", "attribute__name")
autocomplete_fields = ["attribute"]
@admin.register(Category)
class CategoryAdmin(TranslationFieldsetMixin, DraggableMPTTAdmin, BasicModelAdmin):
mptt_indent_field = "name"
list_display = ("indented_title", "parent", "is_active", "modified")
list_filter = ("is_active", "level", "created", "modified")
list_display_links = ("indented_title",)
search_fields = ("uuid", "name")
inlines = [CategoryChildrenInline]
autocomplete_fields = ["parent", "tags"]
readonly_fields = ("uuid", "slug", "created", "modified")
def indented_title(self, instance):
return instance.name
indented_title.short_description = _("name")
indented_title.admin_order_field = "name"
@admin.register(Brand)
class BrandAdmin(TranslationFieldsetMixin, BasicModelAdmin):
list_display = ("name",)
list_filter = ("categories", "is_active")
search_fields = ("uuid", "name", "categories__name")
readonly_fields = ("uuid", "slug")
@admin.register(Product)
class ProductAdmin(BasicModelAdmin, I18NFieldsetMixin):
class ProductAdmin(TranslationFieldsetMixin, BasicModelAdmin):
list_display = (
"name",
"partnumber",
@ -281,7 +198,6 @@ class ProductAdmin(BasicModelAdmin, I18NFieldsetMixin):
"rating",
"modified",
)
list_filter = (
"is_active",
"is_digital",
@ -290,7 +206,6 @@ class ProductAdmin(BasicModelAdmin, I18NFieldsetMixin):
"created",
"modified",
)
search_fields = (
"name",
"partnumber",
@ -299,69 +214,43 @@ class ProductAdmin(BasicModelAdmin, I18NFieldsetMixin):
"uuid",
"slug",
)
readonly_fields = ("created", "modified", "uuid", "rating", "price", "slug")
autocomplete_fields = ("category", "brand", "tags")
fieldsets = (
(
"general",
{"fields": ("name", "slug", "partnumber", "is_active", "is_digital")},
),
("relations", {"fields": ("category", "brand", "tags")}),
("metadata", {"fields": ("uuid",)}),
("timestamps", {"fields": ("created", "modified")}),
)
inlines = [AttributeValueInline, ProductImageInline, StockInline]
def price(self, obj):
return obj.price
price.short_description = _("price") # type: ignore
price.short_description = _("price")
def rating(self, obj):
return obj.rating
rating.short_description = _("rating") # type: ignore
fieldsets = (
(
SECTION_GENERAL,
{
"fields": (
"name",
"slug",
"partnumber",
"is_active",
"is_digital",
),
},
),
(
SECTION_RELATIONS,
{
"fields": ("category", "brand", "tags"),
},
),
(
SECTION_META,
{
"fields": ("uuid",),
},
),
(
SECTION_DATES,
{
"fields": ("created", "modified"),
},
),
)
inlines = [AttributeValueInline, ProductImageInline, StockInline]
rating.short_description = _("rating")
def get_changelist(self, request, **kwargs):
changelist = super().get_changelist(request, **kwargs)
changelist.filter_input_length = 64
return changelist
cl = super().get_changelist(request, **kwargs)
cl.filter_input_length = 64
return cl
@admin.register(ProductTag)
class ProductTagAdmin(BasicModelAdmin, I18NFieldsetMixin):
class ProductTagAdmin(TranslationFieldsetMixin, BasicModelAdmin):
list_display = ("name",)
search_fields = ("name",)
@admin.register(CategoryTag)
class CategoryTagAdmin(BasicModelAdmin, I18NFieldsetMixin):
class CategoryTagAdmin(TranslationFieldsetMixin, BasicModelAdmin):
list_display = ("name",)
search_fields = ("name",)
@ -375,27 +264,12 @@ class VendorAdmin(BasicModelAdmin):
@admin.register(Feedback)
class FeedbackAdmin(BasicModelAdmin):
class FeedbackAdmin(TranslationFieldsetMixin, BasicModelAdmin):
list_display = ("order_product", "rating", "comment", "modified")
list_filter = ("rating", "is_active")
search_fields = ("order_product__product__name", "comment")
class OrderProductInline(admin.TabularInline):
model = OrderProduct
extra = 0
readonly_fields = ("product", "quantity", "buy_price")
form = OrderProductForm
is_navtab = True
verbose_name = _("order product")
verbose_name_plural = _("order products")
icon = "fa-regular fa-circle-dot"
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related("product").only("product__name")
@admin.register(Order)
class OrderAdmin(BasicModelAdmin):
list_display = (
@ -409,30 +283,33 @@ class OrderAdmin(BasicModelAdmin):
)
list_filter = ("status", "buy_time", "modified", "created")
search_fields = ("user__email", "status", "uuid", "human_readable_id")
readonly_fields = ("total_price", "total_quantity", "buy_time", "human_readable_id")
inlines = [OrderProductInline]
form = OrderForm
readonly_fields = ("total_price", "total_quantity", "buy_time", "human_readable_id")
def is_business(self, obj):
return obj.is_business
is_business.short_description = _("is business") # type: ignore
is_business.short_description = _("is business")
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.prefetch_related(
return (
super()
.get_queryset(request)
.prefetch_related(
"user",
"shipping_address",
"billing_address",
"order_products",
"promo_code",
)
)
def save_model(self, request, obj, form, change):
if form.cleaned_data.get("attributes") is None:
obj.attributes = None
if form.cleaned_data.get("notifications") is None:
obj.attributes = None
obj.notifications = None
super().save_model(request, obj, form, change)
@ -444,14 +321,13 @@ class OrderProductAdmin(BasicModelAdmin):
form = OrderProductForm
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.prefetch_related("order", "product")
return super().get_queryset(request).prefetch_related("order", "product")
def save_model(self, request, obj, form, change):
if form.cleaned_data.get("attributes") is None:
obj.attributes = None
if form.cleaned_data.get("notifications") is None:
obj.attributes = None
obj.notifications = None
super().save_model(request, obj, form, change)
@ -469,19 +345,17 @@ class PromoCodeAdmin(BasicModelAdmin):
search_fields = ("code",)
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.prefetch_related("user")
return super().get_queryset(request).prefetch_related("user")
@admin.register(Promotion)
class PromotionAdmin(BasicModelAdmin, I18NFieldsetMixin):
class PromotionAdmin(TranslationFieldsetMixin, BasicModelAdmin):
list_display = ("name", "discount_percent", "modified")
search_fields = ("name",)
autocomplete_fields = ("products",)
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.prefetch_related("products")
return super().get_queryset(request).prefetch_related("products")
@admin.register(Stock)
@ -499,7 +373,7 @@ class WishlistAdmin(BasicModelAdmin):
@admin.register(ProductImage)
class ProductImageAdmin(BasicModelAdmin):
class ProductImageAdmin(TranslationFieldsetMixin, BasicModelAdmin):
list_display = ("alt", "product", "priority", "modified")
list_filter = ("priority",)
search_fields = ("alt", "product__name")
@ -518,7 +392,6 @@ class AddressAdmin(GISModelAdmin):
"user__email",
"address_line",
)
gis_widget_kwargs = {
"attrs": {
"default_lon": 37.61556,
@ -528,21 +401,7 @@ class AddressAdmin(GISModelAdmin):
}
class ConstanceAdmin(BaseConstanceAdmin):
def get_urls(self):
info = f"{self.model._meta.app_label}_{self.model._meta.model_name}"
return [
path(
"",
self.admin_site.admin_view(self.changelist_view),
name=f"{info}_changelist",
),
path(
"", self.admin_site.admin_view(self.changelist_view), name=f"{info}_add"
),
]
# Constance config
class ConstanceConfig:
class Meta:
app_label = "core"
@ -554,9 +413,6 @@ class ConstanceConfig:
swapped = False
is_composite_pk = False
def get_ordered_objects(self):
return False
def get_change_permission(self):
return f"change_{self.model_name}"
@ -572,12 +428,14 @@ class ConstanceConfig:
def label_lower(self):
return f"{self.app_label}.{self.model_name}"
def get_ordered_objects(self):
return False
_meta = Meta()
admin.site.unregister([Config]) # type: ignore
admin.site.register([ConstanceConfig], ConstanceAdmin) # type: ignore
admin.site.site_title = f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]}" # type: ignore
admin.site.unregister([Config])
admin.site.register([ConstanceConfig], BaseConstanceAdmin)
admin.site.site_title = f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]}"
admin.site.site_header = "eVibes"
admin.site.index_title = f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]}" # type: ignore
admin.site.index_title = f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]}"