From c682137fc5d581026063ef62dc873f6c40ff3c23 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Sun, 22 Jun 2025 16:15:48 +0300 Subject: [PATCH] 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. --- core/abstract.py | 4 - core/admin.py | 420 ++++++++++++++++------------------------------- 2 files changed, 139 insertions(+), 285 deletions(-) diff --git a/core/abstract.py b/core/abstract.py index 7749093b..7ee83069 100644 --- a/core/abstract.py +++ b/core/abstract.py @@ -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( diff --git a/core/admin.py b/core/admin.py index 1a8bea48..4c63f113 100644 --- a/core/admin.py +++ b/core/admin.py @@ -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( - "user", - "shipping_address", - "billing_address", - "order_products", - "promo_code", + 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]}"