from contextlib import suppress from typing import Any, Callable, ClassVar, Type 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 import admin 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, TextField from django.db.models.query import QuerySet from django.http import HttpRequest from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from django_celery_beat.admin import ClockedScheduleAdmin as BaseClockedScheduleAdmin from django_celery_beat.admin import CrontabScheduleAdmin as BaseCrontabScheduleAdmin from django_celery_beat.admin import PeriodicTaskAdmin as BasePeriodicTaskAdmin from django_celery_beat.admin import PeriodicTaskForm, TaskSelectWidget from django_celery_beat.models import ( ClockedSchedule, CrontabSchedule, IntervalSchedule, PeriodicTask, SolarSchedule, ) from djangoql.admin import DjangoQLSearchMixin from import_export.admin import ImportExportModelAdmin 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.contrib.import_export.forms import ExportForm, ImportForm from unfold.decorators import action from unfold.typing import FieldsetsType from unfold.widgets import UnfoldAdminSelectWidget, UnfoldAdminTextInputWidget from unfold_markdown import MarkdownWidget from engine.core.forms import ( CRMForm, OrderForm, OrderProductForm, StockForm, VendorForm, ) from engine.core.models import ( Address, Attribute, AttributeGroup, AttributeValue, Brand, Category, CategoryTag, CustomerRelationshipManagementProvider, Feedback, Order, OrderCrmLink, OrderProduct, PastedImage, Product, ProductImage, ProductTag, PromoCode, Promotion, Stock, Vendor, Wishlist, ) class FieldsetsMixin: general_fields: list[str] | None = [] relation_fields: list[str] | None = [] additional_fields: list[str] | None = [] model: ClassVar[Type[Model]] def get_fieldsets(self, request: HttpRequest, obj: Any = None) -> FieldsetsType: if request: pass if obj: pass fieldsets = [] def add_translations_fieldset( fss: FieldsetsType, ) -> FieldsetsType: with suppress(NotRegistered): transoptions = translator.get_options_for_model(self.model) translation_fields = [] for orig in transoptions.local_fields: translation_fields += get_translation_fields(orig) if translation_fields: fss = list(fss) + [ ( _("translations"), {"classes": ["tab"], "fields": translation_fields}, ) ] return fss if self.general_fields: fieldsets.append( (_("general"), {"classes": ["tab"], "fields": self.general_fields}) ) if self.relation_fields: fieldsets.append( (_("relations"), {"classes": ["tab"], "fields": self.relation_fields}) ) if self.additional_fields: fieldsets.append( ( _("additional info"), {"classes": ["tab"], "fields": self.additional_fields}, ) ) opts = self.model._meta meta_fields = [] if any(f.name == "uuid" for f in opts.fields): meta_fields.append("uuid") if any(f.name == "slug" for f in opts.fields): meta_fields.append("slug") if any(f.name == "sku" for f in opts.fields): meta_fields.append("sku") if any(f.name == "human_readable_id" for f in opts.fields): meta_fields.append("human_readable_id") if 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"), {"classes": ["tab"], "fields": ts})) fieldsets = add_translations_fieldset(fieldsets) return fieldsets # noinspection PyUnresolvedReferences class ActivationActionsMixin: message_user: Callable actions_on_top = True actions_on_bottom = True actions = [ "delete_selected", "activate_selected", "deactivate_selected", ] @action( description=_("Activate selected %(verbose_name_plural)s"), # ty:ignore[invalid-argument-type] permissions=["change"], ) def activate_selected(self, request: HttpRequest, queryset: QuerySet[Any]) -> None: try: queryset.update(is_active=True) self.message_user( request=request, message=_("selected items have been activated.").lower().title(), level=messages.SUCCESS, ) except Exception as e: self.message_user(request=request, message=str(e), level=messages.ERROR) @action( description=_("Deactivate selected %(verbose_name_plural)s"), # ty:ignore[invalid-argument-type] permissions=["change"], ) def deactivate_selected( self, request: HttpRequest, queryset: QuerySet[Any] ) -> None: try: queryset.update(is_active=False) self.message_user( request=request, message=_("selected items have been deactivated.").lower().title(), level=messages.SUCCESS, ) except Exception as e: self.message_user(request=request, message=str(e), level=messages.ERROR) class AttributeValueInline(TabularInline): model = AttributeValue extra = 0 autocomplete_fields = ["attribute"] verbose_name = _("attribute value") verbose_name_plural = _("attribute values") tab = True def get_queryset(self, request): return super().get_queryset(request).select_related("attribute", "product") class ProductImageInline(TabularInline): model = ProductImage extra = 0 tab = True verbose_name = _("image") verbose_name_plural = _("images") def get_queryset(self, request): return super().get_queryset(request).select_related("product") class StockInline(TabularInline): model = Stock extra = 0 form = StockForm tab = True verbose_name = _("stock") verbose_name_plural = _("stocks") def get_queryset(self, request): return super().get_queryset(request).select_related("vendor", "product") class OrderProductInline(TabularInline): model = OrderProduct extra = 0 readonly_fields = ("product", "quantity", "buy_price") form = OrderProductForm verbose_name = _("order product") verbose_name_plural = _("order products") tab = True 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") tab = True verbose_name = _("children") verbose_name_plural = _("children") @register(AttributeGroup) class AttributeGroupAdmin( DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin ): # noinspection PyClassVar model = AttributeGroup list_display = ( "name", "modified", ) search_fields = ( "uuid", "name", ) readonly_fields = ( "uuid", "modified", "created", ) general_fields = [ "is_active", "name", "parent", ] @register(Attribute) class AttributeAdmin( DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin ): # noinspection PyClassVar model = Attribute list_display = ( "name", "group", "value_type", "modified", ) list_filter = ( "value_type", "group", "is_active", ) search_fields = ( "uuid", "name", "group__name", ) readonly_fields = ( "uuid", "modified", "created", ) autocomplete_fields = [ "group", ] general_fields = [ "is_active", "name", "value_type", "is_filterable", ] relation_fields = [ "group", ] @register(AttributeValue) class AttributeValueAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # noinspection PyClassVar model = AttributeValue list_display = ( "attribute", "value", "modified", ) list_filter = ( "attribute__group", "is_active", ) search_fields = ( "uuid", "value", "attribute__name", ) readonly_fields = ( "uuid", "modified", "created", ) autocomplete_fields = [ "attribute", ] general_fields = [ "is_active", "value", ] relation_fields = [ "attribute", "product", ] @register(Category) class CategoryAdmin( DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, DraggableMPTTAdmin, ModelAdmin, ): # noinspection PyClassVar model = Category formfield_overrides = {TextField: {"widget": MarkdownWidget}} list_display = ( "indented_title", "parent", "is_active", "modified", ) # noinspection PyUnresolvedReferences list_filter = ( "is_active", "level", "created", "modified", ) search_fields = ( "uuid", "name", ) inlines = [ CategoryChildrenInline, ] autocomplete_fields = [ "parent", "tags", ] readonly_fields = ( "slug", "uuid", "modified", "created", ) general_fields = [ "is_active", "name", "description", "image", "markup_percent", "priority", ] relation_fields = [ "parent", "tags", ] @register(Brand) class BrandAdmin( DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin ): # noinspection PyClassVar model = Brand formfield_overrides = {TextField: {"widget": MarkdownWidget}} list_display = ( "name", "priority", "is_active", ) list_filter = ("is_active",) search_fields = ( "uuid", "name", ) readonly_fields = ( "uuid", "slug", "modified", "created", ) general_fields = [ "is_active", "name", "description", "priority", ] additional_fields = ["small_logo", "big_logo"] @register(Product) class ProductAdmin( DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin, ImportExportModelAdmin, ): # noinspection PyClassVar model = Product formfield_overrides = {TextField: {"widget": MarkdownWidget}} actions = ActivationActionsMixin.actions + [ "export_to_marketplaces", "ban_from_marketplaces", ] list_display = ( "sku", "name", "is_active", "export_to_marketplaces", "has_images", "category", "brand", "price", "modified", ) list_filter = ( "is_active", "is_digital", "is_updatable", "stocks__vendor", "tags__name", "created", "modified", ) search_fields = ( "name", "partnumber", "brand__name", "brand__slug", "category__name", "category__slug", "uuid", "slug", "sku", ) readonly_fields = ( "sku", "slug", "uuid", "modified", "created", "video_preview", ) autocomplete_fields = ( "category", "brand", "tags", ) inlines = [ AttributeValueInline, ProductImageInline, StockInline, ] import_form_class = ImportForm export_form_class = ExportForm general_fields = [ "is_active", "name", "partnumber", "is_digital", "export_to_marketplaces", ] relation_fields = [ "category", "brand", "tags", ] additional_fields = [ "is_updatable", "video", "video_preview", ] def video_preview(self, obj: Product): if obj.video: return format_html( '", obj.video.url, ) return "—" video_preview.short_description = _("video preview") # ty:ignore[unresolved-attribute] def has_images(self, obj: Product) -> bool: return obj.has_images has_images.boolean = True # ty:ignore[unresolved-attribute] has_images.short_description = _("has images") # ty:ignore[unresolved-attribute] @action( description=_("Export selected %(verbose_name_plural)s to marketplaces' feeds"), # ty:ignore[invalid-argument-type] permissions=["change"], ) def export_to_marketplaces( self, request: HttpRequest, queryset: QuerySet[Any] ) -> None: try: queryset.update(export_to_marketplaces=True) self.message_user( request=request, message=_( "selected %(verbose_name_plural)s have been marked for export." ) .lower() .title(), level=messages.SUCCESS, ) except Exception as e: self.message_user(request=request, message=str(e), level=messages.ERROR) @action( description=_("Ban selected %(verbose_name_plural)s from marketplaces' feeds"), # ty:ignore[invalid-argument-type] permissions=["change"], ) def ban_from_marketplaces( self, request: HttpRequest, queryset: QuerySet[Any] ) -> None: try: queryset.update(export_to_marketplaces=False) self.message_user( request=request, message=_( "selected %(verbose_name_plural)s have been banned from export." ) .lower() .title(), level=messages.SUCCESS, ) except Exception as e: self.message_user(request=request, message=str(e), level=messages.ERROR) def get_queryset(self, request): return ( super() .get_queryset(request) .select_related("category", "brand") .prefetch_related( "tags", "images", "stocks__vendor", "attributes__attribute", ) ) @register(ProductTag) class ProductTagAdmin( DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin ): # noinspection PyClassVar model = ProductTag list_display = ("tag_name",) search_fields = ("tag_name",) readonly_fields = ( "uuid", "modified", "created", ) general_fields = [ "is_active", "tag_name", "name", ] @register(CategoryTag) class CategoryTagAdmin( DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin ): # noinspection PyClassVar model = CategoryTag list_display = ( "name", "tag_name", "is_active", ) search_fields = ( "name", "tag_name", "is_active", ) readonly_fields = ( "uuid", "modified", "created", ) general_fields = [ "is_active", "tag_name", "name", ] @register(Vendor) class VendorAdmin( DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin ): # noinspection PyClassVar model = Vendor list_display = ( "name", "markup_percent", "modified", ) list_filter = ( "markup_percent", "is_active", ) search_fields = ( "name", "uuid", ) readonly_fields = ( "uuid", "modified", "created", "last_processing_response", ) form = VendorForm general_fields = [ "is_active", "name", "markup_percent", "authentication", ] relation_fields = [ "users", ] additional_fields = [ "integration_path", "last_processing_response", "b2b_auth_token", ] @register(Feedback) class FeedbackAdmin( DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin ): # noinspection PyClassVar model = Feedback list_display = ( "order_product", "rating", "comment", "modified", ) list_filter = ( "rating", "is_active", ) search_fields = ( "order_product__product__name", "comment", ) readonly_fields = ( "uuid", "modified", "created", ) general_fields = [ "is_active", "rating", "comment", ] relation_fields = [ "order_product", ] @register(Order) class OrderAdmin( DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin ): # noinspection PyClassVar model = Order list_display = ( "human_readable_id", "user", "status", "total_price", "buy_time", "modified", ) list_filter = ( "status", "buy_time", "modified", "created", ) search_fields = ( "user__email", "status", "uuid", "human_readable_id", ) readonly_fields = ( "total_price", "total_quantity", "human_readable_id", "uuid", "modified", "created", ) inlines = [ OrderProductInline, ] form = OrderForm general_fields = [ "is_active", "user", "status", "notifications", "attributes", "buy_time", ] relation_fields = [ "promo_code", "billing_address", "shipping_address", ] @register(OrderProduct) class OrderProductAdmin( DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin ): # noinspection PyClassVar model = OrderProduct list_display = ( "order", "product", "quantity", "buy_price", "status", "modified", ) list_filter = ( "status", "modified", ) search_fields = ( "order__user__email", "product__name", ) readonly_fields = ( "uuid", "modified", "created", ) form = OrderProductForm general_fields = [ "is_active", "quantity", "buy_price", "status", ] relation_fields = [ "order", "product", ] @register(PromoCode) class PromoCodeAdmin( DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin ): # noinspection PyClassVar model = PromoCode list_display = ( "code", "discount_percent", "discount_amount", "start_time", "end_time", "used_on", ) list_filter = ( "discount_percent", "discount_amount", "start_time", "end_time", ) search_fields = ( "code", "uuid", "user__email", ) readonly_fields = ( "used_on", "uuid", "modified", "created", ) autocomplete_fields = ("user",) general_fields = [ "is_active", "code", "discount_amount", "discount_percent", "start_time", "end_time", "used_on", ] relation_fields = [ "user", ] @register(Promotion) class PromotionAdmin( DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin ): # noinspection PyClassVar model = Promotion formfield_overrides = {TextField: {"widget": MarkdownWidget}} list_display = ( "name", "discount_percent", "modified", ) search_fields = ("name",) readonly_fields = ( "uuid", "modified", "created", ) autocomplete_fields = ("products",) general_fields = [ "is_active", "name", "discount_percent", "description", ] relation_fields = [ "products", ] @register(Stock) class StockAdmin( DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin ): # noinspection PyClassVar model = Stock form = StockForm list_display = ( "product", "vendor", "sku", "quantity", "price", "modified", ) list_filter = ( "vendor", "quantity", ) search_fields = ( "product__name", "vendor__name", "sku", ) readonly_fields = ( "uuid", "modified", "created", ) autocomplete_fields = ( "product", "vendor", ) general_fields = [ "is_active", "sku", "quantity", "purchase_price", "price", "digital_asset", ] additional_fields = [ "system_attributes", ] relation_fields = [ "product", "vendor", ] @register(Wishlist) class WishlistAdmin( DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin ): # noinspection PyClassVar model = Wishlist list_display = ( "user", "modified", ) search_fields = ( "user__email", "uuid", ) readonly_fields = ( "uuid", "modified", "created", ) general_fields = [ "is_active", "user", ] relation_fields = [ "products", ] @register(ProductImage) class ProductImageAdmin( DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin ): # noinspection PyClassVar model = ProductImage list_display = ( "alt", "product", "priority", "modified", ) list_filter = ( "priority", "modified", "created", ) search_fields = ( "alt", "product__name", ) readonly_fields = ( "uuid", "modified", "created", ) autocomplete_fields = ("product",) general_fields = [ "is_active", "alt", "priority", "image", ] relation_fields = [ "product", ] @register(PastedImage) class PastedImageAdmin(ActivationActionsMixin, ModelAdmin): list_display = ("name", "image_preview", "alt_text", "is_active", "created") list_filter = ("is_active",) search_fields = ("name", "alt_text") readonly_fields = ("uuid", "created", "modified", "image_preview_large") fieldsets = ( (None, {"fields": ("name", "image", "alt_text")}), (_("Preview"), {"fields": ("image_preview_large",)}), (_("Metadata"), {"fields": ("uuid", "is_active", "created", "modified")}), ) @admin.display(description=_("preview")) def image_preview(self, obj: PastedImage) -> str: if obj.image: return format_html( '', obj.image.url, ) return "-" @admin.display(description=_("image preview")) def image_preview_large(self, obj: PastedImage) -> str: if obj.image: return format_html( '', obj.image.url, ) return "-" @register(Address) class AddressAdmin(DjangoQLSearchMixin, FieldsetsMixin, GISModelAdmin): # noinspection PyClassVar model = Address list_display = ( "street", "city", "region", "country", "user", ) # country and region are encrypted — DB-level filtering is not possible list_filter = () # street, city, postal_code are encrypted — DB-level search is not possible search_fields = ("user__email",) readonly_fields = ( "uuid", "modified", "created", ) gis_widget_kwargs = { "attrs": { "default_lon": 37.61556, "default_lat": 55.75222, "default_zoom": 6, } } general_fields = [ "is_active", "address_line", "street", "district", "city", "region", "postal_code", "country", "raw_data", ] relation_fields = [ "user", "api_response", ] @register(CustomerRelationshipManagementProvider) class CustomerRelationshipManagementProviderAdmin( DjangoQLSearchMixin, FieldsetsMixin, ModelAdmin ): # noinspection PyClassVar model = CustomerRelationshipManagementProvider list_display = ( "name", "default", ) search_fields = ( "name", "uuid", ) readonly_fields = ( "uuid", "modified", "created", ) form = CRMForm general_fields = [ "is_active", "name", "default", "integration_url", "integration_location", "attributes", "authentication", ] @register(OrderCrmLink) class OrderCrmLinkAdmin(DjangoQLSearchMixin, FieldsetsMixin, ModelAdmin): # noinspection PyClassVar model = OrderCrmLink list_display = ( "crm_lead_id", "order", ) search_fields = ( "crm_lead_id", "order__human_readable_id", "order__uuid", "order__user__uuid", "order__user__email", ) readonly_fields = ( "uuid", "modified", "created", "crm_lead_id", ) general_fields = [ "is_active", "crm_lead_id", ] relation_fields = [ "order", "crm", ] # Constance configuration class ConstanceConfig: class Meta: app_label = "core" object_name = "Config" concrete_model = None model_name = module_name = "config" verbose_name_plural = _("Config") abstract = False swapped = False is_composite_pk = False def get_change_permission(self) -> str: return f"change_{self.model_name}" @property def app_config(self) -> AppConfig: return apps.get_app_config(self.app_label) @property def label(self) -> str: return f"{self.app_label}.{self.object_name}" @property def label_lower(self) -> str: return f"{self.app_label}.{self.model_name}" def get_ordered_objects(self) -> bool: return False _meta = Meta() site.unregister([Config]) # ty:ignore[invalid-argument-type] site.register([ConstanceConfig], BaseConstanceAdmin) # ty:ignore[invalid-argument-type] site.site_title = settings.PROJECT_NAME site.site_header = "Schon" site.index_title = settings.PROJECT_NAME site.unregister(PeriodicTask) site.unregister(IntervalSchedule) site.unregister(CrontabSchedule) site.unregister(SolarSchedule) site.unregister(ClockedSchedule) class UnfoldTaskSelectWidget(UnfoldAdminSelectWidget, TaskSelectWidget): pass class UnfoldPeriodicTaskForm(PeriodicTaskForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["task"].widget = UnfoldAdminTextInputWidget() self.fields["regtask"].widget = UnfoldTaskSelectWidget() @register(PeriodicTask) class PeriodicTaskAdmin(BasePeriodicTaskAdmin, ModelAdmin): form = UnfoldPeriodicTaskForm @register(IntervalSchedule) class IntervalScheduleAdmin(ModelAdmin): pass @register(CrontabSchedule) class CrontabScheduleAdmin(BaseCrontabScheduleAdmin, ModelAdmin): pass @register(SolarSchedule) class SolarScheduleAdmin(ModelAdmin): pass @register(ClockedSchedule) class ClockedScheduleAdmin(BaseClockedScheduleAdmin, ModelAdmin): pass