This commit introduces support for uploading optional video files to products and image files to blog posts. Enhanced admin interfaces were added to preview these files directly. Also includes adjustments to GraphQL types and serializers to expose the new fields.
1239 lines
29 KiB
Python
1239 lines
29 KiB
Python
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(
|
|
'<video controls style="max-width:100%;max-height:360px">'
|
|
'<source src="{}">'
|
|
"</video>",
|
|
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(
|
|
'<img src="{}" style="max-height: 50px; max-width: 100px;" />',
|
|
obj.image.url,
|
|
)
|
|
return "-"
|
|
|
|
@admin.display(description=_("image preview"))
|
|
def image_preview_large(self, obj: PastedImage) -> str:
|
|
if obj.image:
|
|
return format_html(
|
|
'<img src="{}" style="max-height: 300px; max-width: 500px;" />',
|
|
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
|