from django.contrib import admin, messages from django import forms import logging from django.contrib.postgres.aggregates import StringAgg from django.db.models import Value from django.db.models.functions import Coalesce logger = logging.getLogger(__name__) # from product.tasks import update_prices from .models import * from unfold.admin import TabularInline, StackedInline from home.models import LearnVideoModel from import_export.admin import ImportExportModelAdmin from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget from django.contrib.postgres.fields import ArrayField from unfold.widgets import UnfoldAdminColorInputWidget from unfold.decorators import action, display from utils.admin import ModelAdmin from django.shortcuts import redirect, render from django.utils.html import format_html from .permissions import ProductDetailCategoryPermission, ProductAdminPermission, ProductVariantAdminPermission, ProductVariantInlineAdminPermission, InPackItemsAdminPermission, AttributeTypeAdminPermission, AttributeValueAdminPermission from django import forms from django.core.cache import cache # Cache key for home page HOME_CACHE_KEY = 'home_view_data_anonymous' @admin.register(ProductDetailCategory) class ProductDetailCategoryAdmin(ProductDetailCategoryPermission, ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm search_fields = ['title'] compressed_fields = True warn_unsaved_form = True formfield_overrides = { ArrayField: { "widget": ArrayWidget, } } @admin.register(UnitCategoryModel) class UnitCategoryAdmin(ModelAdmin): pass @admin.register(InPackItems) class InPackItemsAdmin(InPackItemsAdminPermission, ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm search_fields = ['item_title'] compressed_fields = True warn_unsaved_form = True formfield_overrides = { ArrayField: { "widget": ArrayWidget, } } from account.models import ShopModel @admin.register(ShopModel) class ShopModelAdmin(ModelAdmin, ImportExportModelAdmin): search_fields = ['name'] compressed_fields = True warn_unsaved_form = True formfield_overrides = { ArrayField: { "widget": ArrayWidget, } } def get_queryset(self, request): if request.user.is_superuser: return ShopModel.objects.all() if not hasattr(request.user, 'shop'): return ShopModel.objects.none() queryset = ShopModel.objects.filter(id=request.user.shop.id) return queryset def has_view_permission(self, request, obj=None): if request.user.is_superuser or obj == None: return True if not hasattr(request.user, 'shop'): return False return request.user.shop == obj def get_readonly_fields(self, request, obj = ...): if request.user.is_superuser: return [] else: return ['user', 'shop_name', 'commission_percent'] def has_change_permission(self, request, obj=None): return True class AttributeValueInLine(StackedInline): model = AttributeValue extra = 0 show_change_link = True min_num = 1 # autocomplete_fields = ['product_attributes', 'in_pack_items', 'images'] # search_fields = [''] def has_view_permission(self, request, obj = ...): return True def has_add_permission(self, request, obj): return True def has_change_permission(self, request, obj = ...): return False def has_delete_permission(self, request, obj = ...): return False @admin.register(AttributeType) class AttributeTypeAdmin(AttributeTypeAdminPermission, ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm search_fields = ['name'] compressed_fields = True warn_unsaved_form = True inlines = [AttributeValueInLine] formfield_overrides = { ArrayField: { "widget": ArrayWidget, } } @admin.register(AttributeValue) class AttributeValueAdmin(AttributeValueAdminPermission, ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm search_fields = ['value', 'attribute_type__name'] compressed_fields = True warn_unsaved_form = True formfield_overrides = { ArrayField: { "widget": ArrayWidget, } } # def get_form(self, request, obj=None, change=False, **kwargs): # form = super().get_form(request, obj, change, **kwargs) # form.base_fields["color"].widget = UnfoldAdminColorInputWidget() # return form @admin.register(ProductImageModel) class ProductImagesAdmin(ModelAdmin): import_form_class = ImportForm export_form_class = ExportForm search_fields = ['name'] compressed_fields = True warn_unsaved_form = True list_display = ['display_image', 'name',] formfield_overrides = { ArrayField: { "widget": ArrayWidget, } } def has_view_permission(self, request, obj=None): return True def has_add_permission(self, request, obj=None): return True @display(description='محصول', header=True) def display_image(self, instance): if instance and instance.image: image = instance.image.url else: image = None return [ instance.name, None, None, { "path": image, "height": 30, "width": 30, "borderless": True, # "squared": True, }, ] @admin.register(DetailModel) class DetailModelAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm search_fields = ['title'] compressed_fields = True warn_unsaved_form = True formfield_overrides = { ArrayField: { "widget": ArrayWidget, } } class DetailInLine(StackedInline): model = DetailModel extra = 0 show_change_link = True min_num = 1 max_num = 4 def has_add_permission(self, request, obj = ...): return True def has_change_permission(self, request, obj = ...): return True def has_view_permission(self, request, obj = ...): return True def has_delete_permission(self, request, obj = ...): return True from unfold.widgets import UnfoldAdminTextInputWidget # --- ProductVariantAdminForm for price formatting --- class ProductVariantAdminForm(forms.ModelForm): class Meta: model = ProductVariant fields = "__all__" widgets = { "input_price": UnfoldAdminTextInputWidget(attrs={"class": "price-input"}), "min_price": UnfoldAdminTextInputWidget(attrs={"class": "price-input"}), "profit": UnfoldAdminTextInputWidget(attrs={"class": "price-input"}), } @admin.register(ProductDetailModel) class ProductDetailModel1Admin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm search_fields = ['detail_category__title', 'name'] compressed_fields = True warn_unsaved_form = True autocomplete_fields = ['detail_category',] inlines = [DetailInLine] formfield_overrides = { ArrayField: { "widget": ArrayWidget, } } def has_view_permission(self, request, obj=None): return True def has_add_permission(self, request, obj=None): return True def get_queryset(self, request): if request.user.is_superuser: logger.info('Returning all ProductDetailModels for superuser') return ProductDetailModel.objects.all() if not hasattr(request.user, 'shop'): logger.info('User has no shop, returning empty queryset') return ProductDetailModel.objects.none() logger.info('Filtering ProductDetailModels by shop') queryset = ProductDetailModel.objects.filter(product__product__shop__id=request.user.shop.id) return queryset def has_change_permission(self, request, obj = ...): return True def has_delete_permission(self, request, obj = ...): return True class ProductVariantInLine(ProductVariantInlineAdminPermission, StackedInline): model = ProductVariant form = ProductVariantAdminForm extra = 0 show_change_link = True tab = True min_num = 1 readonly_fields = ['price', 'sell', 'price_in_dollor'] # inlines = [DetailModelInLine] autocomplete_fields = ['product_attributes', 'in_pack_items', 'images', 'details'] fields = ['input_price', 'min_price', 'in_stock', 'price', 'currency', 'profit', 'discount', 'special_discount_percent', 'images', 'video', 'color', 'product_attributes', 'in_pack_items', 'details', 'sell', 'slider_category'] # search_fields = [''] def formfield_for_dbfield(self, db_field, request, **kwargs): if db_field.name == 'color': kwargs['widget'] = UnfoldAdminColorInputWidget() return super().formfield_for_dbfield(db_field, request, **kwargs) def get_readonly_fields(self, request, obj = ...): if request.user.is_superuser: return ['price', 'sell', 'price_in_dollor'] else: return ['price', 'sell', 'price_in_dollor', 'slider_category'] def save_formset(self, request, form, formset, change): super().save_formset(request, form, formset, change) cache.delete(HOME_CACHE_KEY) from unfold.contrib.filters.admin import RelatedDropdownFilter class BulkSubCategoryForm(forms.Form): """فرم برای انتخاب زیر دسته‌بندی برای محصولات""" subcategory = forms.ModelChoiceField( queryset=SubCategoryModel.objects.all(), required=True, label='زیر دسته‌بندی', help_text='زیر دسته‌بندی جدید را برای محصولات انتخاب شده انتخاب کنید' ) @admin.register(ProductVariant) class ProductVariantAdmin(ProductVariantAdminPermission, ModelAdmin, ImportExportModelAdmin): form = ProductVariantAdminForm class Media: js = ("price-format.js",) import_form_class = ImportForm export_form_class = ExportForm autocomplete_fields = ['product_attributes', 'images', 'in_pack_items', 'details'] warn_unsaved_form = True readonly_fields = ['price', 'created_at'] list_filter = [('product__category', RelatedDropdownFilter), ('product__category__parent', RelatedDropdownFilter), ('product', RelatedDropdownFilter)] list_filter_submit = True list_display = ('product', 'created_at') # inlines = [DetailModelInLine] def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) cache.delete(HOME_CACHE_KEY) from unfold.sections import TableSection class VariantsTableSection(TableSection): verbose_name = 'تنوع های محصولی' related_name = "variants" height = 380 fields = [ 'get_name', "price", 'in_stock', 'sell', ] # @display(description='برگذار شده', label=True) # def get_done(self, obj): # from django.utils import timezone # now = timezone.localtime() # today = now.date() # time = now.time() # return obj.date > today or (obj.date == today and obj.time > time) @display(description='نام تنوع') def get_name(self, obj): attrs = getattr(obj, "attrs", "") return f"{obj.product.name} - {attrs}" def get_queryset(self, request): qs = super().get_queryset(request) qs = qs.select_related("product").annotate( attrs=Coalesce( StringAgg("product_attributes__value", delimiter=", "), Value("") ) ) return qs.only( "id", "product__name", "price", "in_stock", "sell", ) @admin.register(ProductModel) class ProductModelAdmin(ProductAdminPermission, ModelAdmin, ImportExportModelAdmin): list_sections = [VariantsTableSection] import_form_class = ImportForm export_form_class = ExportForm inlines = [ProductVariantInLine] readonly_fields = ('slug', 'created_at') search_fields = ['name', 'description', ] list_per_page = 10 list_filter = [('category', RelatedDropdownFilter), 'show_in_bot', ('category__parent', RelatedDropdownFilter)] list_filter_submit = True autocomplete_fields = ['related_products', 'shop', 'category'] # compressed_fields = True warn_unsaved_form = True # list_per_page = 2 actions_list = ['redirect_to_learn', 'update_products_price', 'resync_all_torob'] list_display = ['display_image', 'shop__shop_name', 'view', 'rating', 'category', 'created_at' ,'show_in_website', ] fieldsets = ( ('فیلد های اصلی', {'fields': ('name', 'description', 'category', 'image', 'related_products','show_in_trends', 'show_in_most_viewed', 'show_in_lot_of_discount', 'show_in_top_seller', 'shop', 'show_in_bot', 'bot_banner'), "classes": ["tab"],}), ('فیلد های سيو', {'fields': ('meta_description', 'meta_keywords', 'meta_rating', 'slug'), "classes": ["tab"],}), ('فیلد های مربوط به کاربر', {'fields': ('rating', 'view',), "classes": ["tab"],}), # ('فیلد های ایتم های پک', {'fields': ('in_pack_items', ), "classes": ["tab"],}) ) formfield_overrides = { models.TextField: { "widget": WysiwygWidget, }, ArrayField: { "widget": ArrayWidget, } } def get_readonly_fields(self, request, obj=None): if request.user.is_superuser: return ['slug', 'created_at'] else: return ['show_in_bot', 'bot_banner', 'created_at', 'show_in_top_seller','show_in_trends', 'show_in_most_viewed', 'show_in_lot_of_discount','meta_description', 'meta_keywords', 'meta_rating', 'rating', 'view', 'slug'] def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) cache.delete(HOME_CACHE_KEY) def get_queryset(self, request): qs = super().get_queryset(request) qs = qs.select_related( "shop", "category", "category__parent", ).only( "id", "name", "description", "image", "view", "rating", "created_at", "show_in_bot", "category", "shop", ) return qs def show_in_website(self, obj): url = f"https://heymlz.com/product/{obj.slug}" return format_html('نمایش', url) show_in_website.short_description = 'نمایش در سایت' @display(description='محصول', header=True) def display_image(self, instance): if instance and instance.variants.first() and instance.variants.first().images.first(): image = instance.variants.first().images.first().image.url if instance.variants.first().images.first().image else None else: image = None return [ instance.name, None, None, { "path": image, "height": 30, "width": 30, "borderless": True, # "squared": True, }, ] @action(description=f"اپدیت قیمت ها") def update_products_price(self, request): from product.tasks import update_prices update_prices() messages.success(request, f"قیمت {ProductVariant.objects.all().count()} تنوع محصول اپدیت شد") return redirect("admin:product_productmodel_changelist") @action(description="ارسال مجدد همه محصولات به ترب") def resync_all_torob(self, request): from django.conf import settings from product.tasks import send_torob_product_webhook if not getattr(settings, "TOROB_PRODUCT_WEBHOOK_TOKEN", None): messages.error(request, "توکن وبهوک ترب در تنظیمات وجود ندارد") return redirect("admin:product_productmodel_changelist") product_ids = list( ProductModel.objects.exclude(slug__isnull=True).exclude(slug="").values_list("id", flat=True) ) if not product_ids: messages.warning(request, "محصولی برای ارسال یافت نشد") return redirect("admin:product_productmodel_changelist") chunk_size = 50 queued = 0 for start in range(0, len(product_ids), chunk_size): send_torob_product_webhook.delay(product_ids[start:start + chunk_size]) queued += 1 messages.success( request, f"{len(product_ids)} محصول در {queued} بسته برای ارسال به ترب در صف قرار گرفت", ) return redirect("admin:product_productmodel_changelist") def resync_selected_torob(self, request, queryset): from django.conf import settings from product.tasks import send_torob_product_webhook if not getattr(settings, "TOROB_PRODUCT_WEBHOOK_TOKEN", None): messages.error(request, "توکن وبهوک ترب در تنظیمات وجود ندارد") return product_ids = list( queryset.exclude(slug__isnull=True).exclude(slug="").values_list("id", flat=True) ) if not product_ids: messages.warning(request, "محصول معتبری انتخاب نشد") return chunk_size = 50 for start in range(0, len(product_ids), chunk_size): send_torob_product_webhook.delay(product_ids[start:start + chunk_size]) messages.success(request, f"{len(product_ids)} محصول برای ارسال به ترب در صف قرار گرفت") resync_selected_torob.short_description = "ارسال محصولات انتخاب شده به ترب" def bulk_update_subcategory_action(self, request, queryset): """اکشن برای تغییر دسته‌بندی چند محصول همزمان""" # اگر فرم ارسال شده است if 'apply' in request.POST: form = BulkSubCategoryForm(request.POST) if form.is_valid(): subcategory = form.cleaned_data['subcategory'] count = queryset.count() # به‌روزرسانی تمام محصولات انتخاب شده queryset.update(category=subcategory) messages.success( request, f'دسته‌بندی {count} محصول به "{subcategory.name}" تغییر یافت.' ) return redirect('admin:product_productmodel_changelist') else: form = BulkSubCategoryForm() # نمایش صفحه تأیید context = { 'form': form, 'products': queryset, 'selected_ids': list(queryset.values_list('pk', flat=True)), 'action_name': 'bulk_update_subcategory_action', 'title': 'تغییر دسته‌بندی محصولات', } return render( request, 'admin/product/bulk_subcategory_form.html', context ) bulk_update_subcategory_action.short_description = "تغییر دسته‌بندی محصولات انتخاب شده" actions = ['bulk_update_subcategory_action', 'resync_selected_torob'] @admin.register(MainCategoryModel) class MainCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm list_display = ['name', ] readonly_fields = ('slug', ) search_fields = ['name', 'slug'] compressed_fields = True warn_unsaved_form = True formfield_overrides = { ArrayField: { "widget": ArrayWidget, } } def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) cache.delete(HOME_CACHE_KEY) cache.delete('all_categories_v2') @admin.register(SubCategoryModel) class SubCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin): list_display = ['name', 'parent'] search_fields = ['name', 'slug'] list_filter = ['parent', ] import_form_class = ImportForm export_form_class = ExportForm readonly_fields = ('slug', ) compressed_fields = True warn_unsaved_form = True formfield_overrides = { ArrayField: { "widget": ArrayWidget, } } def has_view_permission(self, request, obj = ...): return True def has_add_permission(self, request): return True def has_change_permission(self, request, obj=None): return True def has_delete_permission(self, request, obj=None): return False def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) cache.delete(HOME_CACHE_KEY) cache.delete('all_categories_v2') @admin.register(CommentModel) class CommentAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm list_display = ['user', 'product', 'display_content','review_status'] search_fields = ['content',] list_filter = ['review_status',] compressed_fields = True warn_unsaved_form = True formfield_overrides = { ArrayField: { "widget": ArrayWidget, } } radio_fields = {'review_status': admin.VERTICAL} def display_content(self, obj): return obj.content[0:35] + '...' display_content.short_description = 'محتوای کامنت' def has_view_permission(self, request, obj = ...): return request.user.is_superuser def has_add_permission(self, request): return request.user.is_superuser def has_change_permission(self, request, obj=None): return request.user.is_superuser def has_delete_permission(self, request, obj=None): return request.user.is_superuser @admin.register(DollorModel) class DollorAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm exclude = ('unique_filed', ) compressed_fields = True warn_unsaved_form = True formfield_overrides = { ArrayField: { "widget": ArrayWidget, } } readonly_fields = ('price',) @admin.register(ProductRating) class ProductRatingAdmin(ModelAdmin): list_display = ('product', 'user', 'rating', 'created_at') list_filter = ('rating', 'created_at') search_fields = ('product__name', 'user__phone', 'user__first_name', 'user__last_name') readonly_fields = ('product', 'user', 'created_at', 'updated_at') date_hierarchy = 'created_at' ordering = ('-created_at',) compressed_fields = True warn_unsaved_form = True def has_view_permission(self, request, obj = ...): return request.user.is_superuser def has_add_permission(self, request): return request.user.is_superuser def has_change_permission(self, request, obj=None): return request.user.is_superuser def has_delete_permission(self, request, obj=None): return request.user.is_superuser