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, default=uuid.uuid4,
editable=False, editable=False,
) )
uuid.id_for_label = "uuid"
is_active: bool = BooleanField( # type: ignore is_active: bool = BooleanField( # type: ignore
default=True, default=True,
verbose_name=_("is active"), 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" "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: datetime = CreationDateTimeField( # type: ignore
_("created"), help_text=_("when the object first appeared on the database") _("created"), help_text=_("when the object first appeared on the database")
) )
created.id_for_label = "created"
modified: datetime = ModificationDateTimeField( # type: ignore modified: datetime = ModificationDateTimeField( # type: ignore
_("modified"), help_text=_("when the object was last modified") _("modified"), help_text=_("when the object was last modified")
) )
modified.id_for_label = "modified"
def save(self, **kwargs): def save(self, **kwargs):
self.update_modified = kwargs.pop( 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.apps import apps
from django.contrib import admin from django.contrib import admin
from django.contrib.admin import ModelAdmin, TabularInline from django.contrib.admin import ModelAdmin, TabularInline
from django.contrib.admin.options import InlineModelAdmin
from django.contrib.gis.admin import GISModelAdmin from django.contrib.gis.admin import GISModelAdmin
from django.db.models import Model 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 django.utils.translation import gettext_lazy as _
from modeltranslation.translator import translator from modeltranslation.translator import translator
from modeltranslation.utils import get_translation_fields from modeltranslation.utils import get_translation_fields
@ -38,72 +35,37 @@ from .models import (
Wishlist, Wishlist,
) )
SECTION_GENERAL = _("general")
SECTION_I18N = _("I18N")
SECTION_META = _("metadata")
SECTION_DATES = _("timestamps")
SECTION_RELATIONS = _("relations")
class TranslationFieldsetMixin:
model: Model
class I18NFieldsetMixin: def get_fieldsets(self, request, obj=None):
model: type[Model] fieldsets = super().get_fieldsets(request, obj)
opts = translator.get_options_for_model(self.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)
translation_fields = [] translation_fields = []
for orig in trans_opts.local_fields: for orig in opts.local_fields:
translation_fields += get_translation_fields(orig) translation_fields += get_translation_fields(orig)
if translation_fields:
class _TranslationInline(InlineModelAdmin[self.model, self.model]): fieldsets = list(fieldsets)
model = self.model fieldsets.append(
form = modelform_factory( (
self.model, _("translations"),
fields=translation_fields, {"fields": translation_fields},
formfield_callback=lambda f, **kw: super(
I18NFieldsetMixin, self
).formfield_for_dbfield(f, request, **kw),
) )
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()
) )
return fieldsets
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
class BasicModelAdmin(ModelAdmin): class BasicModelAdmin(ModelAdmin):
@admin.action(description=str(_("activate selected %(verbose_name_plural)s"))) @admin.action(description=str(_("activate selected %(verbose_name_plural)s")))
def activate_selected(self, _request, queryset) -> str: def activate_selected(self, _request, queryset) -> str:
queryset.update(is_active=True) queryset.update(is_active=True)
return "" return str(_("%(verbose_name_plural)s activated successfully!"))
@admin.action(description=str(_("deactivate selected %(verbose_name_plural)s"))) @admin.action(description=str(_("deactivate selected %(verbose_name_plural)s")))
def deactivate_selected(self, _request, queryset) -> str: def deactivate_selected(self, _request, queryset) -> str:
queryset.update(is_active=False) queryset.update(is_active=False)
return "" return str(_("%(verbose_name_plural)s deactivated successfully."))
def get_actions(self, request): def get_actions(self, request):
actions = super().get_actions(request) actions = super().get_actions(request)
@ -130,134 +92,13 @@ class AttributeValueInline(TabularInline):
icon = "fa-regular fa-circle-dot" 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): class ProductImageInline(TabularInline):
model = ProductImage model = ProductImage
extra = 0 extra = 0
is_navtab = True is_navtab = True
verbose_name = _("image") verbose_name = _("image")
verbose_name_plural = _("images") verbose_name_plural = _("images")
icon = "fa-regular fa-circle-dot" icon = "fa-regular fa-images"
class StockInline(TabularInline): class StockInline(TabularInline):
@ -266,11 +107,87 @@ class StockInline(TabularInline):
is_navtab = True is_navtab = True
verbose_name = _("stock") verbose_name = _("stock")
verbose_name_plural = _("stocks") 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" 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) @admin.register(Product)
class ProductAdmin(BasicModelAdmin, I18NFieldsetMixin): class ProductAdmin(TranslationFieldsetMixin, BasicModelAdmin):
list_display = ( list_display = (
"name", "name",
"partnumber", "partnumber",
@ -281,7 +198,6 @@ class ProductAdmin(BasicModelAdmin, I18NFieldsetMixin):
"rating", "rating",
"modified", "modified",
) )
list_filter = ( list_filter = (
"is_active", "is_active",
"is_digital", "is_digital",
@ -290,7 +206,6 @@ class ProductAdmin(BasicModelAdmin, I18NFieldsetMixin):
"created", "created",
"modified", "modified",
) )
search_fields = ( search_fields = (
"name", "name",
"partnumber", "partnumber",
@ -299,69 +214,43 @@ class ProductAdmin(BasicModelAdmin, I18NFieldsetMixin):
"uuid", "uuid",
"slug", "slug",
) )
readonly_fields = ("created", "modified", "uuid", "rating", "price", "slug") readonly_fields = ("created", "modified", "uuid", "rating", "price", "slug")
autocomplete_fields = ("category", "brand", "tags") 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): def price(self, obj):
return obj.price return obj.price
price.short_description = _("price") # type: ignore price.short_description = _("price")
def rating(self, obj): def rating(self, obj):
return obj.rating return obj.rating
rating.short_description = _("rating") # type: ignore rating.short_description = _("rating")
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]
def get_changelist(self, request, **kwargs): def get_changelist(self, request, **kwargs):
changelist = super().get_changelist(request, **kwargs) cl = super().get_changelist(request, **kwargs)
changelist.filter_input_length = 64 cl.filter_input_length = 64
return changelist return cl
@admin.register(ProductTag) @admin.register(ProductTag)
class ProductTagAdmin(BasicModelAdmin, I18NFieldsetMixin): class ProductTagAdmin(TranslationFieldsetMixin, BasicModelAdmin):
list_display = ("name",) list_display = ("name",)
search_fields = ("name",) search_fields = ("name",)
@admin.register(CategoryTag) @admin.register(CategoryTag)
class CategoryTagAdmin(BasicModelAdmin, I18NFieldsetMixin): class CategoryTagAdmin(TranslationFieldsetMixin, BasicModelAdmin):
list_display = ("name",) list_display = ("name",)
search_fields = ("name",) search_fields = ("name",)
@ -375,27 +264,12 @@ class VendorAdmin(BasicModelAdmin):
@admin.register(Feedback) @admin.register(Feedback)
class FeedbackAdmin(BasicModelAdmin): class FeedbackAdmin(TranslationFieldsetMixin, BasicModelAdmin):
list_display = ("order_product", "rating", "comment", "modified") list_display = ("order_product", "rating", "comment", "modified")
list_filter = ("rating", "is_active") list_filter = ("rating", "is_active")
search_fields = ("order_product__product__name", "comment") 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) @admin.register(Order)
class OrderAdmin(BasicModelAdmin): class OrderAdmin(BasicModelAdmin):
list_display = ( list_display = (
@ -409,30 +283,33 @@ class OrderAdmin(BasicModelAdmin):
) )
list_filter = ("status", "buy_time", "modified", "created") list_filter = ("status", "buy_time", "modified", "created")
search_fields = ("user__email", "status", "uuid", "human_readable_id") search_fields = ("user__email", "status", "uuid", "human_readable_id")
readonly_fields = ("total_price", "total_quantity", "buy_time", "human_readable_id")
inlines = [OrderProductInline] inlines = [OrderProductInline]
form = OrderForm form = OrderForm
readonly_fields = ("total_price", "total_quantity", "buy_time", "human_readable_id")
def is_business(self, obj): def is_business(self, obj):
return obj.is_business return obj.is_business
is_business.short_description = _("is business") # type: ignore is_business.short_description = _("is business")
def get_queryset(self, request): def get_queryset(self, request):
qs = super().get_queryset(request) return (
return qs.prefetch_related( super()
.get_queryset(request)
.prefetch_related(
"user", "user",
"shipping_address", "shipping_address",
"billing_address", "billing_address",
"order_products", "order_products",
"promo_code", "promo_code",
) )
)
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
if form.cleaned_data.get("attributes") is None: if form.cleaned_data.get("attributes") is None:
obj.attributes = None obj.attributes = None
if form.cleaned_data.get("notifications") is None: if form.cleaned_data.get("notifications") is None:
obj.attributes = None obj.notifications = None
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
@ -444,14 +321,13 @@ class OrderProductAdmin(BasicModelAdmin):
form = OrderProductForm form = OrderProductForm
def get_queryset(self, request): def get_queryset(self, request):
qs = super().get_queryset(request) return super().get_queryset(request).prefetch_related("order", "product")
return qs.prefetch_related("order", "product")
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
if form.cleaned_data.get("attributes") is None: if form.cleaned_data.get("attributes") is None:
obj.attributes = None obj.attributes = None
if form.cleaned_data.get("notifications") is None: if form.cleaned_data.get("notifications") is None:
obj.attributes = None obj.notifications = None
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
@ -469,19 +345,17 @@ class PromoCodeAdmin(BasicModelAdmin):
search_fields = ("code",) search_fields = ("code",)
def get_queryset(self, request): def get_queryset(self, request):
qs = super().get_queryset(request) return super().get_queryset(request).prefetch_related("user")
return qs.prefetch_related("user")
@admin.register(Promotion) @admin.register(Promotion)
class PromotionAdmin(BasicModelAdmin, I18NFieldsetMixin): class PromotionAdmin(TranslationFieldsetMixin, BasicModelAdmin):
list_display = ("name", "discount_percent", "modified") list_display = ("name", "discount_percent", "modified")
search_fields = ("name",) search_fields = ("name",)
autocomplete_fields = ("products",) autocomplete_fields = ("products",)
def get_queryset(self, request): def get_queryset(self, request):
qs = super().get_queryset(request) return super().get_queryset(request).prefetch_related("products")
return qs.prefetch_related("products")
@admin.register(Stock) @admin.register(Stock)
@ -499,7 +373,7 @@ class WishlistAdmin(BasicModelAdmin):
@admin.register(ProductImage) @admin.register(ProductImage)
class ProductImageAdmin(BasicModelAdmin): class ProductImageAdmin(TranslationFieldsetMixin, BasicModelAdmin):
list_display = ("alt", "product", "priority", "modified") list_display = ("alt", "product", "priority", "modified")
list_filter = ("priority",) list_filter = ("priority",)
search_fields = ("alt", "product__name") search_fields = ("alt", "product__name")
@ -518,7 +392,6 @@ class AddressAdmin(GISModelAdmin):
"user__email", "user__email",
"address_line", "address_line",
) )
gis_widget_kwargs = { gis_widget_kwargs = {
"attrs": { "attrs": {
"default_lon": 37.61556, "default_lon": 37.61556,
@ -528,21 +401,7 @@ class AddressAdmin(GISModelAdmin):
} }
class ConstanceAdmin(BaseConstanceAdmin): # Constance config
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"
),
]
class ConstanceConfig: class ConstanceConfig:
class Meta: class Meta:
app_label = "core" app_label = "core"
@ -554,9 +413,6 @@ class ConstanceConfig:
swapped = False swapped = False
is_composite_pk = False is_composite_pk = False
def get_ordered_objects(self):
return False
def get_change_permission(self): def get_change_permission(self):
return f"change_{self.model_name}" return f"change_{self.model_name}"
@ -572,12 +428,14 @@ class ConstanceConfig:
def label_lower(self): def label_lower(self):
return f"{self.app_label}.{self.model_name}" return f"{self.app_label}.{self.model_name}"
def get_ordered_objects(self):
return False
_meta = Meta() _meta = Meta()
admin.site.unregister([Config]) # type: ignore admin.site.unregister([Config])
admin.site.register([ConstanceConfig], ConstanceAdmin) # type: ignore admin.site.register([ConstanceConfig], BaseConstanceAdmin)
admin.site.site_title = f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]}"
admin.site.site_title = f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]}" # type: ignore
admin.site.site_header = "eVibes" 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]}"