diff --git a/backend/account/admin.py b/backend/account/admin.py index 3e81445..07e62f7 100644 --- a/backend/account/admin.py +++ b/backend/account/admin.py @@ -27,14 +27,14 @@ class UserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin): inlines = [UserAddressInLine] list_filter = ['is_superuser'] search_fields = ['phone', 'first_name', 'last_name', 'email'] - list_display = ['full_name_display', 'phone', 'email', 'is_superuser', ] + list_display = ['full_name_display', 'phone', 'email', 'is_superuser', 'gender', 'birth_date'] # readonly_fields = ['phone', 'email', 'otp_expiry', 'otp_hash', 'date_joined', 'profile_photo'] - exclude = ('otp_hash', 'otp_expiry', 'is_active', 'is_staff', 'password', 'last_login') + exclude = ('otp_hash', 'otp_expiry', 'is_active', 'is_staff', 'password', 'last_login',) import_form_class = ImportForm export_form_class = ExportForm fieldsets = ( - ('اطلاعات شخصی', {'fields': ('first_name', 'last_name', 'profile_photo', 'password'),}), + ('اطلاعات شخصی', {'fields': ('first_name', 'last_name', 'profile_photo', 'password', 'gender', 'birth_date'),}), ('اطلاعات ارتباطی', {'fields': ('phone', 'email'),}), ) empty_value_display = 'ثبت نشده' diff --git a/backend/account/migrations/0006_user_brith_day_user_gender.py b/backend/account/migrations/0006_user_brith_day_user_gender.py new file mode 100644 index 0000000..e7d61c3 --- /dev/null +++ b/backend/account/migrations/0006_user_brith_day_user_gender.py @@ -0,0 +1,25 @@ +# Generated by Django 5.1.2 on 2025-02-11 17:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0005_alter_useraddressmodel_options_alter_user_is_active_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='brith_day', + field=models.DateField(default='2024-10-09'), + preserve_default=False, + ), + migrations.AddField( + model_name='user', + name='gender', + field=models.CharField(choices=[('male', 'مرد'), ('female', 'زن')], default='male', max_length=20, verbose_name='جنسیت'), + preserve_default=False, + ), + ] diff --git a/backend/account/migrations/0007_rename_brith_day_user_birth_day.py b/backend/account/migrations/0007_rename_brith_day_user_birth_day.py new file mode 100644 index 0000000..b6b8e74 --- /dev/null +++ b/backend/account/migrations/0007_rename_brith_day_user_birth_day.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-02-11 18:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0006_user_brith_day_user_gender'), + ] + + operations = [ + migrations.RenameField( + model_name='user', + old_name='brith_day', + new_name='birth_day', + ), + ] diff --git a/backend/account/migrations/0008_rename_birth_day_user_birth_date.py b/backend/account/migrations/0008_rename_birth_day_user_birth_date.py new file mode 100644 index 0000000..b4a4d46 --- /dev/null +++ b/backend/account/migrations/0008_rename_birth_day_user_birth_date.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-02-11 18:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0007_rename_brith_day_user_birth_day'), + ] + + operations = [ + migrations.RenameField( + model_name='user', + old_name='birth_day', + new_name='birth_date', + ), + ] diff --git a/backend/account/migrations/0009_alter_user_gender.py b/backend/account/migrations/0009_alter_user_gender.py new file mode 100644 index 0000000..ed7e2b0 --- /dev/null +++ b/backend/account/migrations/0009_alter_user_gender.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-02-11 18:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0008_rename_birth_day_user_birth_date'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='gender', + field=models.CharField(choices=[('مرد', 'مرد'), ('زن', 'زن')], max_length=20, verbose_name='جنسیت'), + ), + ] diff --git a/backend/account/models.py b/backend/account/models.py index 74d4ad9..9038461 100644 --- a/backend/account/models.py +++ b/backend/account/models.py @@ -41,6 +41,12 @@ class User(AbstractBaseUser, PermissionsMixin): profile_photo = models.ImageField(upload_to='profile_photos/', null=True, blank=True, verbose_name='عکس پروفایل') is_active = models.BooleanField(default=True, verbose_name='فعال بودن کاربر') is_staff = models.BooleanField(default=False, verbose_name='کارمند') + gender_option = ( + ('مرد', 'مرد'), + ('زن', 'زن') + ) + gender = models.CharField(choices=gender_option, max_length=20, verbose_name='جنسیت') + birth_date = models.DateField() date_joined = models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ثبتنام') otp_hash = models.CharField(max_length=64, null=True, blank=True, verbose_name='کد یک بار مصرف') otp_expiry = models.DateTimeField(null=True, blank=True, verbose_name='تاریخ تمام شدن کد یک بار مصرف') diff --git a/backend/account/serializers.py b/backend/account/serializers.py index dae6e9e..0bb0e30 100644 --- a/backend/account/serializers.py +++ b/backend/account/serializers.py @@ -10,7 +10,7 @@ class CustomTokenObtainPairSerializer(TokenObtainPairSerializer): class ProfileSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ['first_name', 'last_name', 'email', 'profile_photo', 'phone'] + fields = ['first_name', 'last_name', 'email', 'profile_photo', 'phone', 'gender', 'birth_date'] read_only_fields = ("phone",) diff --git a/backend/product/admin.py b/backend/product/admin.py index 96aaceb..334e46f 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -77,11 +77,24 @@ class AttributeValueAdmin(ModelAdmin, ImportExportModelAdmin): "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 + # 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 + + formfield_overrides = { + ArrayField: { + "widget": ArrayWidget, + } + } @@ -92,14 +105,14 @@ class ProductVariantInLine(StackedInline): show_change_link = True tab = True min_num = 1 - autocomplete_fields = ['attributes'] + autocomplete_fields = ['attributes', 'in_pack_items', 'images'] # search_fields = [''] - 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 + 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) @@ -138,15 +151,15 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin): readonly_fields = ('slug', ) search_fields = ['name', 'description', ] list_filter = ['show', 'category'] - autocomplete_fields = ['related_products', 'in_pack_items',] + autocomplete_fields = ['related_products', ] # compressed_fields = True warn_unsaved_form = True - list_display = ['display_image', 'display_price', 'view', 'show', 'rating', 'category', ] + list_display = ['display_price', 'view', 'show', 'rating', 'category', ] fieldsets = ( ('فیلد های اصلی', {'fields': ('name', 'description', 'category', 'related_products', 'show',), "classes": ["tab"],}), ('فیلد های سيو', {'fields': ('meta_description', 'meta_keywords', 'meta_rating', 'slug'), "classes": ["tab"],}), ('فیلد های مربوط به کاربر', {'fields': ('rating', 'view',), "classes": ["tab"],}), - ('فیلد های ایتم های پک', {'fields': ('in_pack_items', ), "classes": ["tab"],}) + # ('فیلد های ایتم های پک', {'fields': ('in_pack_items', ), "classes": ["tab"],}) ) formfield_overrides = { @@ -163,24 +176,24 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin): return obj.get_toman_price() display_price.short_description = 'قیمت تومانی' - @display(description='محصول', header=True) - def display_image(self, instance): - if instance and instance.variants.first() and instance.variants.first().attributes.first(): - image = instance.variants.first().attributes.first().image.url if instance.variants.first().attributes.first().image else None - else: - image = None - return [ - instance.name, - None, - None, - { - "path": image, - "height": 30, - "width": 30, - "borderless": True, - # "squared": True, - }, - ] + # @display(description='محصول', header=True) + # def display_image(self, instance): + # if instance and instance.variants.first() and instance.variants.first().attributes.first(): + # image = instance.variants.first().attributes.first().image.url if instance.variants.first().attributes.first().image else None + # else: + # image = None + # return [ + # instance.name, + # None, + # None, + # { + # "path": image, + # "height": 30, + # "width": 30, + # "borderless": True, + # # "squared": True, + # }, + # ] # @display( # description=("نمایش در صفحه ی اصلی"), # label={ diff --git a/backend/product/migrations/0021_remove_productmodel_in_pack_items_and_more.py b/backend/product/migrations/0021_remove_productmodel_in_pack_items_and_more.py new file mode 100644 index 0000000..1e9785c --- /dev/null +++ b/backend/product/migrations/0021_remove_productmodel_in_pack_items_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.2 on 2025-02-11 17:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0020_productvariant_max_price_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='productmodel', + name='in_pack_items', + ), + migrations.AddField( + model_name='productvariant', + name='in_pack_items', + field=models.ManyToManyField(blank=True, to='product.inpackitems', verbose_name='ایتم های داخل پک'), + ), + ] diff --git a/backend/product/migrations/0022_productimagemodel_remove_attributevalue_color_and_more.py b/backend/product/migrations/0022_productimagemodel_remove_attributevalue_color_and_more.py new file mode 100644 index 0000000..280d9c6 --- /dev/null +++ b/backend/product/migrations/0022_productimagemodel_remove_attributevalue_color_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 5.1.2 on 2025-02-11 19:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0021_remove_productmodel_in_pack_items_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ProductImageModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=30, verbose_name='نام عکس')), + ('image', models.ImageField(upload_to='product_images/')), + ], + ), + migrations.RemoveField( + model_name='attributevalue', + name='color', + ), + migrations.RemoveField( + model_name='attributevalue', + name='image', + ), + migrations.RemoveField( + model_name='attributevalue', + name='video', + ), + migrations.AddField( + model_name='productvariant', + name='color', + field=models.CharField(blank=True, max_length=7, null=True, verbose_name='رنک'), + ), + migrations.AddField( + model_name='productvariant', + name='video', + field=models.FileField(blank=True, null=True, upload_to='product_videos/', verbose_name='ویدیو'), + ), + migrations.AddField( + model_name='productvariant', + name='images', + field=models.ManyToManyField(to='product.productimagemodel', verbose_name='عکس ها'), + ), + ] diff --git a/backend/product/models.py b/backend/product/models.py index b148823..1c65209 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -111,7 +111,6 @@ class ProductModel(models.Model): created_at = models.DateTimeField(auto_now_add=True, verbose_name='زمان ثبت محصول') category = models.ForeignKey(SubCategoryModel, null=True, on_delete=models.SET_NULL, related_name='products', verbose_name='دسته بندی محصول') related_products = models.ManyToManyField('self', blank=True, verbose_name='محصولات مرتبط') - in_pack_items = models.ManyToManyField(InPackItems, blank=True, verbose_name='ایتم های داخل پک') @@ -132,9 +131,9 @@ class ProductModel(models.Model): class DetailModel(models.Model): title = models.CharField(max_length=50, verbose_name='عنوان') detail_text1 = models.CharField(max_length=150 , verbose_name='متن جزیات ۱') - detail_text2 = models.CharField(max_length=150 , verbose_name='متن جزیات ۲') - detail_text3 = models.CharField(max_length=150 , verbose_name='متن جزیات ۳') - detail_text4 = models.CharField(max_length=150 , verbose_name='متن جزیات ۴') + detail_text2 = models.CharField(max_length=150 , verbose_name='متن جزیات ۲', blank=True, null=True) + detail_text3 = models.CharField(max_length=150 , verbose_name='متن جزیات ۳', blank=True, null=True) + detail_text4 = models.CharField(max_length=150 , verbose_name='متن جزیات ۴', blank=True, null=True) def __str__(self): return self.title @@ -198,15 +197,27 @@ class AttributeType(models.Model): class AttributeValue(models.Model): attribute_type = models.ForeignKey(AttributeType, on_delete=models.CASCADE, blank=True, null=True) value = models.CharField(verbose_name='مقدار نوع اتربیوت', max_length=100, blank=True, null=True) - color = models.CharField(verbose_name='رنک', max_length=7, blank=True, null=True) - image = models.ImageField(upload_to='product_images/') - video = models.FileField(upload_to='product_videos/', blank=True, null=True, verbose_name='ویدیو') class Meta: unique_together = ('attribute_type', 'value') def __str__(self): return f"{self.attribute_type.name}: {self.value}" + +class ProductImageModel(models.Model): + name = models.CharField(max_length=30, verbose_name='نام عکس') + image = models.ImageField(upload_to='product_images/') + + def __str__(self): + return self.name + + class Meta: + verbose_name = 'عکس محصولات' + verbose_name_plural = 'عکس های محصولات' + + + + class ProductVariant(models.Model): product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='variants', verbose_name='محصول') attributes = models.ManyToManyField(AttributeValue, verbose_name='ویژگی‌ها', related_name='variant') @@ -219,9 +230,13 @@ class ProductVariant(models.Model): ('toman', 'تومان'), ('derham', 'درهم') ) + in_pack_items = models.ManyToManyField(InPackItems, blank=True, verbose_name='ایتم های داخل پک') sell = models.IntegerField(default=0, verbose_name='فروش') currency = models.CharField(verbose_name='نوع ارز', max_length=20, choices=currency_type) discount = models.SmallIntegerField(default=0, verbose_name='تخفیف') + color = models.CharField(verbose_name='رنک', max_length=7, blank=True, null=True) + images = models.ManyToManyField(ProductImageModel, verbose_name='عکس ها') + video = models.FileField(upload_to='product_videos/', blank=True, null=True, verbose_name='ویدیو') class Meta: verbose_name = 'تنوع محصول' verbose_name_plural = 'تنوع‌های محصول' diff --git a/backend/product/serializers.py b/backend/product/serializers.py index 11fc5fc..f3bc036 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -30,18 +30,38 @@ class AttributeValueSerialzier(serializers.ModelSerializer): model = AttributeValue fields = "__all__" + +class InPackItemsSerialzier(serializers.ModelSerializer): + class Meta: + model = InPackItems + fields = '__all__' + +class ProductImageSerailizer(serializers.ModelSerializer): + class Meta: + model = ProductImageModel + fields = '__all__' + + + class ProductVariantSerialzier(serializers.ModelSerializer): attributes = AttributeValueSerialzier(many=True) price = serializers.SerializerMethodField() + in_pack_items = InPackItemsSerialzier(many=True) + images = ProductImageSerailizer(many=True) class Meta: model = ProductVariant - exclude = ('min_price', 'sell', 'currency', 'product') + exclude = ('min_price', 'max_price','sell', 'currency', 'product') def get_price(self, obj): dollor_price = self.context.get('dollor_price') toman_price = obj.get_toman_price(dollor_price=dollor_price) return "{:,.0f} تومان".format(toman_price) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + view_type = self.context.get('view_type', None) + if view_type == 'list': + self.fields.pop('in_pack_items', None) @@ -49,11 +69,12 @@ class ProductVariantSerialzier(serializers.ModelSerializer): class DynamicProductSerializer(serializers.ModelSerializer): - variants = ProductVariantSerialzier(many=True) + variants = serializers.SerializerMethodField() # variants_colors = serializers.SerializerMethodField() is_new = serializers.SerializerMethodField() related_products = serializers.SerializerMethodField() details = ProductDetailSerializer(many=True, read_only=True) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) view_type = self.context.get('view_type', 'all') @@ -74,7 +95,9 @@ class DynamicProductSerializer(serializers.ModelSerializer): 'instance': ['name', 'description', 'rating', 'slug', 'meta_description', 'meta_keywords', 'meta_rating', 'category', 'related_products', 'details', 'in_pack_items', 'variants'], 'chat': ['name', 'description', 'variants'] } - + + def get_variants(self, obj): + return ProductVariantSerialzier(instance=obj.variants.all(), many=True, context=self.context).data # def get_variants_colors(self, obj): # varients = obj.variants.all()