From f5766aa319c9ab2281b2a50e6700bb33c67d6eca Mon Sep 17 00:00:00 2001 From: A'zamov Samandar Date: Sat, 6 Dec 2025 21:50:28 +0500 Subject: [PATCH] update --- core/apps/accounts/migrations/0001_initial.py | 104 ++++- core/apps/accounts/migrations/0002_initial.py | 38 ++ .../0002_user_account_type_user_is_verify.py | 23 - core/apps/accounts/migrations/0003_address.py | 30 -- .../migrations/0004_business_searchhistory.py | 54 --- .../apps/accounts/migrations/0005_userlike.py | 31 -- ..._address_name_business_address_and_more.py | 28 -- .../0007_notification_usernotification.py | 49 --- .../accounts/migrations/0008_user_avatar.py | 18 - .../migrations/0009_alter_user_avatar.py | 18 - .../migrations/0010_alter_business_user.py | 20 - core/apps/api/admin/__init__.py | 10 +- core/apps/api/admin/ad/__init__.py | 5 +- core/apps/api/admin/ad/ad.py | 15 +- .../{ad_items/ad_images.py => ad/image.py} | 4 +- .../{ad_items/ad_options.py => ad/option.py} | 4 +- .../{ad_items/ad_variant.py => ad/variant.py} | 4 +- core/apps/api/admin/ad_items/__init__.py | 5 - core/apps/api/admin/ad_items/ad_top_plan.py | 12 - core/apps/api/admin/ad_items/tags.py | 12 - core/apps/api/admin/banner/__init__.py | 2 +- core/apps/api/admin/banner/banner.py | 4 +- core/apps/api/admin/category/__init__.py | 2 +- core/apps/api/admin/category/category.py | 4 +- core/apps/api/admin/common.py | 39 ++ core/apps/api/admin/feedback/__init__.py | 2 +- core/apps/api/admin/feedback/feedback.py | 6 +- core/apps/api/choices/__init__.py | 6 +- core/apps/api/filters/__init__.py | 4 +- core/apps/api/filters/ad.py | 70 +-- core/apps/api/filters/category.py | 4 +- core/apps/api/management/__init__.py | 0 core/apps/api/management/commands/__init__.py | 0 .../management/commands/create_fake_data.py | 400 ++++++++++++++++++ core/apps/api/migrations/0001_initial.py | 318 +++++++++++++- ...dmodel_adimage_adoption_adsize_and_more.py | 240 ----------- ...advariantmodel_unique_together_and_more.py | 47 ++ .../api/migrations/0003_category_image.py | 18 - .../api/migrations/0003_colormodel_color.py | 18 + .../migrations/0004_category_category_type.py | 18 - core/apps/api/migrations/0005_admodel_star.py | 18 - .../api/migrations/0006_alter_adimage_ad.py | 19 - .../api/migrations/0007_alter_advariant_ad.py | 19 - .../api/migrations/0008_adimage_ad_variant.py | 19 - .../0009_alter_adimage_ad_variant.py | 19 - .../0010_remove_admodel_star_admodel_image.py | 23 - .../api/migrations/0011_alter_feedback_ad.py | 19 - .../0012_rename_command_feedback_comment.py | 18 - .../migrations/0013_alter_feedback_comment.py | 18 - .../migrations/0014_admodel_description.py | 19 - ...me_en_category_name_ru_category_name_uz.py | 28 -- .../api/migrations/0015_alter_adoption_ad.py | 19 - .../migrations/0016_merge_20251202_1732.py | 14 - core/apps/api/models/__init__.py | 10 +- core/apps/api/models/ad/__init__.py | 8 +- core/apps/api/models/ad/ad.py | 58 ++- core/apps/api/models/ad/category.py | 53 ++- core/apps/api/models/ad/image.py | 35 ++ core/apps/api/models/ad/option.py | 23 + core/apps/api/models/ad/size.py | 24 ++ core/apps/api/models/ad/variant.py | 26 ++ core/apps/api/models/ad_items/__init__.py | 6 - core/apps/api/models/ad_items/ad_images.py | 22 - core/apps/api/models/ad_items/ad_option.py | 18 - core/apps/api/models/ad_items/ad_size.py | 20 - core/apps/api/models/ad_items/ad_top_plan.py | 16 - core/apps/api/models/ad_items/ad_variant.py | 22 - core/apps/api/models/ad_items/tags.py | 15 - core/apps/api/models/banner/__init__.py | 2 +- core/apps/api/models/banner/banner.py | 32 +- core/apps/api/models/common/__init__.py | 4 + core/apps/api/models/common/color.py | 17 + core/apps/api/models/common/plan.py | 26 ++ core/apps/api/models/common/size.py | 16 + core/apps/api/models/common/tags.py | 17 + core/apps/api/models/feedback/__init__.py | 2 +- core/apps/api/models/feedback/feedback.py | 50 ++- core/apps/api/models/order/__init__.py | 2 +- core/apps/api/models/order/order.py | 73 +++- core/apps/api/serializers/__init__.py | 13 +- core/apps/api/serializers/ad/__init__.py | 4 +- core/apps/api/serializers/ad/ad.py | 286 ++++--------- core/apps/api/serializers/ad/home_api.py | 79 +--- core/apps/api/serializers/banner/__init__.py | 2 +- core/apps/api/serializers/banner/banner.py | 5 +- .../apps/api/serializers/category/__init__.py | 2 +- .../apps/api/serializers/category/category.py | 4 +- core/apps/api/serializers/common/__init__.py | 2 + core/apps/api/serializers/common/color.py | 29 ++ core/apps/api/serializers/common/size.py | 28 ++ .../apps/api/serializers/feedback/__init__.py | 1 + .../apps/api/serializers/feedback/feedback.py | 84 ++++ .../api/serializers/notification/__init__.py | 2 +- .../{natification.py => notification.py} | 0 core/apps/api/serializers/order/__init__.py | 1 + core/apps/api/serializers/order/order.py | 149 +++++++ core/apps/api/serializers/search/__init__.py | 4 +- .../search/{search_ads.py => ad.py} | 0 .../search/{search.py => history.py} | 0 core/apps/api/serializers/tags/__init__.py | 1 + core/apps/api/serializers/tags/tags.py | 43 ++ core/apps/api/serializers/user/__init__.py | 2 +- .../serializers/user/{ad_like.py => like.py} | 23 +- core/apps/api/tests/__init__.py | 10 +- core/apps/api/tests/ad/__init__.py | 4 +- core/apps/api/tests/ad/test_ad.py | 2 +- core/apps/api/tests/ad/test_home_api.py | 2 +- core/apps/api/tests/banner/__init__.py | 2 +- core/apps/api/tests/banner/test_banner.py | 6 +- core/apps/api/tests/category/__init__.py | 2 +- core/apps/api/tests/category/test_category.py | 6 +- core/apps/api/tests/search/__init__.py | 4 +- core/apps/api/tests/search/test_search_ads.py | 2 +- core/apps/api/tests/user/__init__.py | 4 +- core/apps/api/translation/__init__.py | 2 +- core/apps/api/translation/category.py | 4 +- core/apps/api/urls.py | 32 +- core/apps/api/views/__init__.py | 16 +- core/apps/api/views/ad/__init__.py | 4 +- core/apps/api/views/ad/ad.py | 85 ++-- core/apps/api/views/ad/home_api.py | 42 +- core/apps/api/views/banner/__init__.py | 2 +- core/apps/api/views/banner/banner.py | 33 +- core/apps/api/views/category/__init__.py | 2 +- core/apps/api/views/category/category.py | 55 ++- core/apps/api/views/common.py | 42 ++ core/apps/api/views/feedback/__init__.py | 1 + core/apps/api/views/feedback/feedback.py | 95 +++++ core/apps/api/views/notification/__init__.py | 2 +- .../api/views/notification/notification.py | 1 - core/apps/api/views/order/__init__.py | 1 + core/apps/api/views/order/order.py | 88 ++++ core/apps/api/views/search/__init__.py | 4 +- .../api/views/search/{search_ads.py => ad.py} | 1 - .../views/search/{search.py => history.py} | 2 +- core/apps/api/views/tags/__init__.py | 1 + core/apps/api/views/tags/tags.py | 30 ++ core/apps/api/views/user/__init__.py | 2 +- core/apps/api/views/user/ad_like.py | 28 -- core/apps/api/views/user/like.py | 40 ++ 140 files changed, 2376 insertions(+), 1582 deletions(-) create mode 100644 core/apps/accounts/migrations/0002_initial.py delete mode 100644 core/apps/accounts/migrations/0002_user_account_type_user_is_verify.py delete mode 100644 core/apps/accounts/migrations/0003_address.py delete mode 100644 core/apps/accounts/migrations/0004_business_searchhistory.py delete mode 100644 core/apps/accounts/migrations/0005_userlike.py delete mode 100644 core/apps/accounts/migrations/0006_rename_address_name_business_address_and_more.py delete mode 100644 core/apps/accounts/migrations/0007_notification_usernotification.py delete mode 100644 core/apps/accounts/migrations/0008_user_avatar.py delete mode 100644 core/apps/accounts/migrations/0009_alter_user_avatar.py delete mode 100644 core/apps/accounts/migrations/0010_alter_business_user.py rename core/apps/api/admin/{ad_items/ad_images.py => ad/image.py} (68%) rename core/apps/api/admin/{ad_items/ad_options.py => ad/option.py} (67%) rename core/apps/api/admin/{ad_items/ad_variant.py => ad/variant.py} (67%) delete mode 100644 core/apps/api/admin/ad_items/__init__.py delete mode 100644 core/apps/api/admin/ad_items/ad_top_plan.py delete mode 100644 core/apps/api/admin/ad_items/tags.py create mode 100644 core/apps/api/admin/common.py create mode 100644 core/apps/api/management/__init__.py create mode 100644 core/apps/api/management/commands/__init__.py create mode 100644 core/apps/api/management/commands/create_fake_data.py delete mode 100644 core/apps/api/migrations/0002_adtopplan_color_tags_admodel_adimage_adoption_adsize_and_more.py create mode 100644 core/apps/api/migrations/0002_sizemodel_alter_advariantmodel_unique_together_and_more.py delete mode 100644 core/apps/api/migrations/0003_category_image.py create mode 100644 core/apps/api/migrations/0003_colormodel_color.py delete mode 100644 core/apps/api/migrations/0004_category_category_type.py delete mode 100644 core/apps/api/migrations/0005_admodel_star.py delete mode 100644 core/apps/api/migrations/0006_alter_adimage_ad.py delete mode 100644 core/apps/api/migrations/0007_alter_advariant_ad.py delete mode 100644 core/apps/api/migrations/0008_adimage_ad_variant.py delete mode 100644 core/apps/api/migrations/0009_alter_adimage_ad_variant.py delete mode 100644 core/apps/api/migrations/0010_remove_admodel_star_admodel_image.py delete mode 100644 core/apps/api/migrations/0011_alter_feedback_ad.py delete mode 100644 core/apps/api/migrations/0012_rename_command_feedback_comment.py delete mode 100644 core/apps/api/migrations/0013_alter_feedback_comment.py delete mode 100644 core/apps/api/migrations/0014_admodel_description.py delete mode 100644 core/apps/api/migrations/0014_category_name_en_category_name_ru_category_name_uz.py delete mode 100644 core/apps/api/migrations/0015_alter_adoption_ad.py delete mode 100644 core/apps/api/migrations/0016_merge_20251202_1732.py create mode 100644 core/apps/api/models/ad/image.py create mode 100644 core/apps/api/models/ad/option.py create mode 100644 core/apps/api/models/ad/size.py create mode 100644 core/apps/api/models/ad/variant.py delete mode 100644 core/apps/api/models/ad_items/__init__.py delete mode 100644 core/apps/api/models/ad_items/ad_images.py delete mode 100644 core/apps/api/models/ad_items/ad_option.py delete mode 100644 core/apps/api/models/ad_items/ad_size.py delete mode 100644 core/apps/api/models/ad_items/ad_top_plan.py delete mode 100644 core/apps/api/models/ad_items/ad_variant.py delete mode 100644 core/apps/api/models/ad_items/tags.py create mode 100644 core/apps/api/models/common/__init__.py create mode 100644 core/apps/api/models/common/color.py create mode 100644 core/apps/api/models/common/plan.py create mode 100644 core/apps/api/models/common/size.py create mode 100644 core/apps/api/models/common/tags.py create mode 100644 core/apps/api/serializers/common/__init__.py create mode 100644 core/apps/api/serializers/common/color.py create mode 100644 core/apps/api/serializers/common/size.py create mode 100644 core/apps/api/serializers/feedback/__init__.py create mode 100644 core/apps/api/serializers/feedback/feedback.py rename core/apps/api/serializers/notification/{natification.py => notification.py} (100%) create mode 100644 core/apps/api/serializers/order/__init__.py create mode 100644 core/apps/api/serializers/order/order.py rename core/apps/api/serializers/search/{search_ads.py => ad.py} (100%) rename core/apps/api/serializers/search/{search.py => history.py} (100%) create mode 100644 core/apps/api/serializers/tags/__init__.py create mode 100644 core/apps/api/serializers/tags/tags.py rename core/apps/api/serializers/user/{ad_like.py => like.py} (54%) create mode 100644 core/apps/api/views/common.py create mode 100644 core/apps/api/views/feedback/__init__.py create mode 100644 core/apps/api/views/feedback/feedback.py create mode 100644 core/apps/api/views/order/__init__.py create mode 100644 core/apps/api/views/order/order.py rename core/apps/api/views/search/{search_ads.py => ad.py} (97%) rename core/apps/api/views/search/{search.py => history.py} (96%) create mode 100644 core/apps/api/views/tags/__init__.py create mode 100644 core/apps/api/views/tags/tags.py delete mode 100644 core/apps/api/views/user/ad_like.py create mode 100644 core/apps/api/views/user/like.py diff --git a/core/apps/accounts/migrations/0001_initial.py b/core/apps/accounts/migrations/0001_initial.py index 872d44c..004b4ec 100644 --- a/core/apps/accounts/migrations/0001_initial.py +++ b/core/apps/accounts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.3 on 2024-12-13 19:04 +# Generated by Django 5.2.7 on 2025-12-06 15:57 import django.db.models.deletion import django.utils.timezone @@ -15,6 +15,51 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='Notification', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('title', models.CharField(max_length=255, verbose_name='Title')), + ('description', models.TextField(verbose_name='Description')), + ('notification_type', models.CharField(choices=[('System', 'System'), ('Another', 'Another')], max_length=255, verbose_name='Type')), + ('long', models.FloatField(verbose_name='Long')), + ('lat', models.FloatField(verbose_name='Lat')), + ], + options={ + 'verbose_name': 'Notification', + 'verbose_name_plural': 'Notifications', + 'db_table': 'notification', + }, + ), + migrations.CreateModel( + name='UserLike', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': 'User Like', + 'verbose_name_plural': 'User Likes', + 'db_table': 'user_like', + }, + ), + migrations.CreateModel( + name='UserNotification', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('is_read', models.BooleanField(default=False, verbose_name='Read')), + ], + options={ + 'verbose_name': 'User Notification', + 'verbose_name_plural': 'User Notifications', + 'db_table': 'user_notification', + }, + ), migrations.CreateModel( name='User', fields=[ @@ -33,7 +78,10 @@ class Migration(migrations.Migration): ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), ('validated_at', models.DateTimeField(blank=True, null=True)), + ('is_verify', models.BooleanField(default=False)), + ('account_type', models.CharField(choices=[('personal', 'Personal'), ('business', 'Business')], default='personal', max_length=255)), ('role', models.CharField(choices=[('superuser', 'Superuser'), ('admin', 'Admin'), ('user', 'User')], default='user', max_length=255)), + ('avatar', models.ImageField(default='avatars/default.png', upload_to='avatars/', verbose_name='Avatar')), ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), ], @@ -43,6 +91,45 @@ class Migration(migrations.Migration): 'abstract': False, }, ), + migrations.CreateModel( + name='Address', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'Address', + 'verbose_name_plural': 'Addresses', + 'db_table': 'address', + }, + ), + migrations.CreateModel( + name='Business', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255, verbose_name='Business Name')), + ('work_time', models.CharField(max_length=255, verbose_name='Work Time')), + ('contact', models.CharField(max_length=255, verbose_name='Contact')), + ('instagram', models.CharField(max_length=255, verbose_name='Instagram')), + ('facebook', models.CharField(max_length=255, verbose_name='Facebook')), + ('telegram', models.CharField(max_length=255, verbose_name='Telegram')), + ('bio', models.TextField(verbose_name='Bio')), + ('address', models.CharField(max_length=255, verbose_name='Address Name')), + ('long', models.FloatField(verbose_name='Longitude')), + ('lat', models.FloatField(verbose_name='Latitude')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='business', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'Business', + 'verbose_name_plural': 'Business', + 'db_table': 'business', + }, + ), migrations.CreateModel( name='ResetToken', fields=[ @@ -57,4 +144,19 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'Reset Tokens', }, ), + migrations.CreateModel( + name='SearchHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.CharField(max_length=255, verbose_name='Search History')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'Search History', + 'verbose_name_plural': 'Search History', + 'db_table': 'search_history', + }, + ), ] diff --git a/core/apps/accounts/migrations/0002_initial.py b/core/apps/accounts/migrations/0002_initial.py new file mode 100644 index 0000000..2e12a07 --- /dev/null +++ b/core/apps/accounts/migrations/0002_initial.py @@ -0,0 +1,38 @@ +# Generated by Django 5.2.7 on 2025-12-06 15:57 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('accounts', '0001_initial'), + ('api', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='userlike', + name='ad', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes', to='api.admodel', verbose_name='Ad'), + ), + migrations.AddField( + model_name='userlike', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes', to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + migrations.AddField( + model_name='usernotification', + name='notification', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.notification', verbose_name='Notification'), + ), + migrations.AddField( + model_name='usernotification', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + ] diff --git a/core/apps/accounts/migrations/0002_user_account_type_user_is_verify.py b/core/apps/accounts/migrations/0002_user_account_type_user_is_verify.py deleted file mode 100644 index 70b9f04..0000000 --- a/core/apps/accounts/migrations/0002_user_account_type_user_is_verify.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-22 07:29 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='account_type', - field=models.CharField(choices=[('personal', 'Personal'), ('business', 'Business')], default='personal', max_length=255), - ), - migrations.AddField( - model_name='user', - name='is_verify', - field=models.BooleanField(default=False), - ), - ] diff --git a/core/apps/accounts/migrations/0003_address.py b/core/apps/accounts/migrations/0003_address.py deleted file mode 100644 index dd85a0d..0000000 --- a/core/apps/accounts/migrations/0003_address.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-22 11:34 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0002_user_account_type_user_is_verify'), - ] - - operations = [ - migrations.CreateModel( - name='Address', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=255, verbose_name='Name')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to=settings.AUTH_USER_MODEL, verbose_name='User')), - ], - options={ - 'verbose_name': 'Address', - 'verbose_name_plural': 'Addresses', - 'db_table': 'address', - }, - ), - ] diff --git a/core/apps/accounts/migrations/0004_business_searchhistory.py b/core/apps/accounts/migrations/0004_business_searchhistory.py deleted file mode 100644 index eb50c95..0000000 --- a/core/apps/accounts/migrations/0004_business_searchhistory.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-24 06:45 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0003_address'), - ] - - operations = [ - migrations.CreateModel( - name='Business', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=255, verbose_name='Business Name')), - ('work_time', models.CharField(max_length=255, verbose_name='Work Time')), - ('contact', models.CharField(max_length=255, verbose_name='Contact')), - ('instagram', models.CharField(max_length=255, verbose_name='Instagram')), - ('facebook', models.CharField(max_length=255, verbose_name='Facebook')), - ('telegram', models.CharField(max_length=255, verbose_name='Telegram')), - ('bio', models.TextField(verbose_name='Bio')), - ('address_name', models.CharField(max_length=255, verbose_name='Address Name')), - ('longitude', models.FloatField(verbose_name='Longitude')), - ('latitude', models.FloatField(verbose_name='Latitude')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Business', - 'verbose_name_plural': 'Business', - 'db_table': 'business', - }, - ), - migrations.CreateModel( - name='SearchHistory', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('value', models.CharField(max_length=255, verbose_name='Search History')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')), - ], - options={ - 'verbose_name': 'Search History', - 'verbose_name_plural': 'Search History', - 'db_table': 'search_history', - }, - ), - ] diff --git a/core/apps/accounts/migrations/0005_userlike.py b/core/apps/accounts/migrations/0005_userlike.py deleted file mode 100644 index 3571446..0000000 --- a/core/apps/accounts/migrations/0005_userlike.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-26 10:04 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0004_business_searchhistory'), - ('api', '0013_alter_feedback_comment'), - ] - - operations = [ - migrations.CreateModel( - name='UserLike', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('ad', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes', to='api.admodel', verbose_name='Ad')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes', to=settings.AUTH_USER_MODEL, verbose_name='User')), - ], - options={ - 'verbose_name': 'User Like', - 'verbose_name_plural': 'User Likes', - 'db_table': 'user_like', - }, - ), - ] diff --git a/core/apps/accounts/migrations/0006_rename_address_name_business_address_and_more.py b/core/apps/accounts/migrations/0006_rename_address_name_business_address_and_more.py deleted file mode 100644 index 3304b6d..0000000 --- a/core/apps/accounts/migrations/0006_rename_address_name_business_address_and_more.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-26 10:06 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0005_userlike'), - ] - - operations = [ - migrations.RenameField( - model_name='business', - old_name='address_name', - new_name='address', - ), - migrations.RenameField( - model_name='business', - old_name='latitude', - new_name='lat', - ), - migrations.RenameField( - model_name='business', - old_name='longitude', - new_name='long', - ), - ] diff --git a/core/apps/accounts/migrations/0007_notification_usernotification.py b/core/apps/accounts/migrations/0007_notification_usernotification.py deleted file mode 100644 index 9093280..0000000 --- a/core/apps/accounts/migrations/0007_notification_usernotification.py +++ /dev/null @@ -1,49 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-26 12:24 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0006_rename_address_name_business_address_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='Notification', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('title', models.CharField(max_length=255, verbose_name='Title')), - ('description', models.TextField(verbose_name='Description')), - ('notification_type', models.CharField(choices=[('System', 'System'), ('Another', 'Another')], max_length=255, verbose_name='Type')), - ('long', models.FloatField(verbose_name='Long')), - ('lat', models.FloatField(verbose_name='Lat')), - ], - options={ - 'verbose_name': 'Notification', - 'verbose_name_plural': 'Notifications', - 'db_table': 'notification', - }, - ), - migrations.CreateModel( - name='UserNotification', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('is_read', models.BooleanField(default=False, verbose_name='Read')), - ('notification', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.notification', verbose_name='Notification')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')), - ], - options={ - 'verbose_name': 'User Notification', - 'verbose_name_plural': 'User Notifications', - 'db_table': 'user_notification', - }, - ), - ] diff --git a/core/apps/accounts/migrations/0008_user_avatar.py b/core/apps/accounts/migrations/0008_user_avatar.py deleted file mode 100644 index 1d15a32..0000000 --- a/core/apps/accounts/migrations/0008_user_avatar.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-28 10:40 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0007_notification_usernotification'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='avatar', - field=models.ImageField(default='resources/static/images/default.png', upload_to='avatars/', verbose_name='Avatar'), - ), - ] diff --git a/core/apps/accounts/migrations/0009_alter_user_avatar.py b/core/apps/accounts/migrations/0009_alter_user_avatar.py deleted file mode 100644 index fdc457c..0000000 --- a/core/apps/accounts/migrations/0009_alter_user_avatar.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-28 10:43 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0008_user_avatar'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='avatar', - field=models.ImageField(default='avatars/default.png', upload_to='avatars/', verbose_name='Avatar'), - ), - ] diff --git a/core/apps/accounts/migrations/0010_alter_business_user.py b/core/apps/accounts/migrations/0010_alter_business_user.py deleted file mode 100644 index 031ecd7..0000000 --- a/core/apps/accounts/migrations/0010_alter_business_user.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-28 11:02 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0009_alter_user_avatar'), - ] - - operations = [ - migrations.AlterField( - model_name='business', - name='user', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='business', to=settings.AUTH_USER_MODEL, verbose_name='User'), - ), - ] diff --git a/core/apps/api/admin/__init__.py b/core/apps/api/admin/__init__.py index ac659ee..d83f40f 100644 --- a/core/apps/api/admin/__init__.py +++ b/core/apps/api/admin/__init__.py @@ -1,5 +1,5 @@ -from .category import * # noqa -from .ad import * # noqa -from .ad_items import * # noqa -from .feedback import * # noqa -from .banner import * # noqa +from .ad import * +from .banner import * +from .category import * +from .common import * # noqa +from .feedback import * diff --git a/core/apps/api/admin/ad/__init__.py b/core/apps/api/admin/ad/__init__.py index 35176b7..039a0bc 100644 --- a/core/apps/api/admin/ad/__init__.py +++ b/core/apps/api/admin/ad/__init__.py @@ -1 +1,4 @@ -from .ad import * # noqa +from .ad import * +from .image import * +from .option import * +from .variant import * diff --git a/core/apps/api/admin/ad/ad.py b/core/apps/api/admin/ad/ad.py index c9a921d..8fc8312 100644 --- a/core/apps/api/admin/ad/ad.py +++ b/core/apps/api/admin/ad/ad.py @@ -1,21 +1,24 @@ from django.contrib import admin -from unfold.admin import ModelAdmin +from unfold.admin import ModelAdmin, TabularInline -from core.apps.api.models import AdModel, AdImage, AdVariant +from core.apps.api.models import AdModel, AdImageModel, AdVariantModel -class AdImageInline(admin.TabularInline): - model = AdImage +class AdImageInline(TabularInline): + model = AdImageModel extra = 1 + tab = True -class AdVariantInline(admin.TabularInline): - model = AdVariant +class AdVariantInline(TabularInline): + model = AdVariantModel extra = 1 + tab = True @admin.register(AdModel) class AdModelAdmin(ModelAdmin): + autocomplete_fields = ["tags"] list_display = ( "id", "__str__", diff --git a/core/apps/api/admin/ad_items/ad_images.py b/core/apps/api/admin/ad/image.py similarity index 68% rename from core/apps/api/admin/ad_items/ad_images.py rename to core/apps/api/admin/ad/image.py index 6e073bb..b7dc309 100644 --- a/core/apps/api/admin/ad_items/ad_images.py +++ b/core/apps/api/admin/ad/image.py @@ -1,10 +1,10 @@ from django.contrib import admin from unfold.admin import ModelAdmin -from core.apps.api.models import AdImage +from core.apps.api.models import AdImageModel -@admin.register(AdImage) +@admin.register(AdImageModel) class AdImageAdmin(ModelAdmin): list_display = ( "id", diff --git a/core/apps/api/admin/ad_items/ad_options.py b/core/apps/api/admin/ad/option.py similarity index 67% rename from core/apps/api/admin/ad_items/ad_options.py rename to core/apps/api/admin/ad/option.py index 93c5436..8926ad8 100644 --- a/core/apps/api/admin/ad_items/ad_options.py +++ b/core/apps/api/admin/ad/option.py @@ -1,10 +1,10 @@ from django.contrib import admin from unfold.admin import ModelAdmin -from core.apps.api.models import AdOption +from core.apps.api.models import AdOptionModel -@admin.register(AdOption) +@admin.register(AdOptionModel) class AdOptionAdmin(ModelAdmin): list_display = ( "id", diff --git a/core/apps/api/admin/ad_items/ad_variant.py b/core/apps/api/admin/ad/variant.py similarity index 67% rename from core/apps/api/admin/ad_items/ad_variant.py rename to core/apps/api/admin/ad/variant.py index 7f60516..a3d46ec 100644 --- a/core/apps/api/admin/ad_items/ad_variant.py +++ b/core/apps/api/admin/ad/variant.py @@ -1,10 +1,10 @@ from django.contrib import admin from unfold.admin import ModelAdmin -from core.apps.api.models import AdVariant +from core.apps.api.models import AdVariantModel -@admin.register(AdVariant) +@admin.register(AdVariantModel) class AdVariantAdmin(ModelAdmin): list_display = ( "id", diff --git a/core/apps/api/admin/ad_items/__init__.py b/core/apps/api/admin/ad_items/__init__.py deleted file mode 100644 index b16fc74..0000000 --- a/core/apps/api/admin/ad_items/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .tags import * # noqa -from .ad_top_plan import * # noqa -from .ad_images import * # noqa -from .ad_variant import * # noqa -from .ad_options import * # noqa diff --git a/core/apps/api/admin/ad_items/ad_top_plan.py b/core/apps/api/admin/ad_items/ad_top_plan.py deleted file mode 100644 index b5b49d2..0000000 --- a/core/apps/api/admin/ad_items/ad_top_plan.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.contrib import admin -from unfold.admin import ModelAdmin - -from core.apps.api.models import AdTopPlan - - -@admin.register(AdTopPlan) -class AdTopPlanAdmin(ModelAdmin): - list_display = ( - "id", - "__str__", - ) diff --git a/core/apps/api/admin/ad_items/tags.py b/core/apps/api/admin/ad_items/tags.py deleted file mode 100644 index f59449a..0000000 --- a/core/apps/api/admin/ad_items/tags.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.contrib import admin -from unfold.admin import ModelAdmin - -from core.apps.api.models import Tags - - -@admin.register(Tags) -class TagsAdmin(ModelAdmin): - list_display = ( - "id", - "__str__", - ) diff --git a/core/apps/api/admin/banner/__init__.py b/core/apps/api/admin/banner/__init__.py index 1b144fd..7490662 100644 --- a/core/apps/api/admin/banner/__init__.py +++ b/core/apps/api/admin/banner/__init__.py @@ -1 +1 @@ -from .banner import * # noqa +from .banner import * diff --git a/core/apps/api/admin/banner/banner.py b/core/apps/api/admin/banner/banner.py index c67e111..f5855bd 100644 --- a/core/apps/api/admin/banner/banner.py +++ b/core/apps/api/admin/banner/banner.py @@ -1,10 +1,10 @@ from django.contrib import admin from unfold.admin import ModelAdmin -from core.apps.api.models import Banner +from core.apps.api.models import BannerModel -@admin.register(Banner) +@admin.register(BannerModel) class BannerAdmin(ModelAdmin): list_display = ( "id", diff --git a/core/apps/api/admin/category/__init__.py b/core/apps/api/admin/category/__init__.py index d63c50f..63ecac4 100644 --- a/core/apps/api/admin/category/__init__.py +++ b/core/apps/api/admin/category/__init__.py @@ -1 +1 @@ -from .category import * # noqa +from .category import * diff --git a/core/apps/api/admin/category/category.py b/core/apps/api/admin/category/category.py index 5cffbde..5fa6961 100644 --- a/core/apps/api/admin/category/category.py +++ b/core/apps/api/admin/category/category.py @@ -1,10 +1,10 @@ from django.contrib import admin from unfold.admin import ModelAdmin -from core.apps.api.models import Category +from core.apps.api.models import CategoryModel -@admin.register(Category) +@admin.register(CategoryModel) class CategoryAdmin(ModelAdmin): list_display = ( "id", diff --git a/core/apps/api/admin/common.py b/core/apps/api/admin/common.py new file mode 100644 index 0000000..41e26ab --- /dev/null +++ b/core/apps/api/admin/common.py @@ -0,0 +1,39 @@ +from django.contrib import admin +from unfold.admin import ModelAdmin + +from core.apps.api.models import ColorModel, SizeModel +from core.apps.api.models import AdTopPlanModel +from core.apps.api.models import TagsModel + + +@admin.register(ColorModel) +class ColorAdmin(ModelAdmin): + list_display = ( + "id", + "__str__", + ) + + +@admin.register(SizeModel) +class SizeAdmin(ModelAdmin): + list_display = ( + "id", + "__str__", + ) + + +@admin.register(AdTopPlanModel) +class AdTopPlanAdmin(ModelAdmin): + list_display = ( + "id", + "__str__", + ) + + +@admin.register(TagsModel) +class TagsAdmin(ModelAdmin): + search_fields = ["name"] + list_display = ( + "id", + "__str__", + ) diff --git a/core/apps/api/admin/feedback/__init__.py b/core/apps/api/admin/feedback/__init__.py index 5e1a07c..c2ed394 100644 --- a/core/apps/api/admin/feedback/__init__.py +++ b/core/apps/api/admin/feedback/__init__.py @@ -1 +1 @@ -from .feedback import * # noqa +from .feedback import * diff --git a/core/apps/api/admin/feedback/feedback.py b/core/apps/api/admin/feedback/feedback.py index d870ca0..88dd026 100644 --- a/core/apps/api/admin/feedback/feedback.py +++ b/core/apps/api/admin/feedback/feedback.py @@ -1,10 +1,10 @@ from django.contrib import admin from unfold.admin import ModelAdmin -from core.apps.api.models import Feedback, FeedbackImages +from core.apps.api.models import FeedbackModel, FeedbackImageModel -@admin.register(Feedback) +@admin.register(FeedbackModel) class FeedbackAdmin(ModelAdmin): list_display = ( "id", @@ -12,7 +12,7 @@ class FeedbackAdmin(ModelAdmin): ) -@admin.register(FeedbackImages) +@admin.register(FeedbackImageModel) class FeedbackImagesAdmin(ModelAdmin): list_display = ( "id", diff --git a/core/apps/api/choices/__init__.py b/core/apps/api/choices/__init__.py index c71ca4f..c7437cd 100644 --- a/core/apps/api/choices/__init__.py +++ b/core/apps/api/choices/__init__.py @@ -1,3 +1,3 @@ -from .ad_type import * # noqa -from .ad_variant_type import * # noqa -from .order_status import * # noqa +from .ad_type import * +from .ad_variant_type import * +from .order_status import * diff --git a/core/apps/api/filters/__init__.py b/core/apps/api/filters/__init__.py index 121b071..6b8a814 100644 --- a/core/apps/api/filters/__init__.py +++ b/core/apps/api/filters/__init__.py @@ -1,2 +1,2 @@ -from .category import * # noqa -from .ad import * # noqa +from .category import * +from .ad import * diff --git a/core/apps/api/filters/ad.py b/core/apps/api/filters/ad.py index 6b8627e..16b08fd 100644 --- a/core/apps/api/filters/ad.py +++ b/core/apps/api/filters/ad.py @@ -1,8 +1,6 @@ from django_filters import rest_framework as filters -from django.db.models import ( - F, Case, When, FloatField, ExpressionWrapper, Subquery, OuterRef -) -from core.apps.api.models import AdVariant, AdModel +from django.db.models import F, Case, When, FloatField, ExpressionWrapper, Subquery, OuterRef +from core.apps.api.models import AdVariantModel, AdModel class AdFilter(filters.FilterSet): @@ -16,70 +14,28 @@ class AdFilter(filters.FilterSet): has_discount = filters.BooleanFilter(method="filter_has_discount") has_normal_user = filters.BooleanFilter(method="filter_has_normal_user") has_business_user = filters.BooleanFilter(method="filter_has_business_user") + discount = filters.BooleanFilter(method="filter_discount") class Meta: model = AdModel fields = ["min_price", "max_price"] + def filter_discount(self, queryset, name, value): + if value is True: + return queryset.filter(discount__gt=0) + return queryset.filter(discount=-1) + def filter_has_business_user(self, queryset, name, value): - return queryset.filter( - user__account_type="business" - ) + return queryset.filter(user__account_type="business") def filter_has_normal_user(self, queryset, name, value): - return queryset.filter( - user__account_type="personal" - ) + return queryset.filter(user__account_type="personal") def filter_has_discount(self, queryset, name, value): - return queryset.filter( - variants__discount__gte=1 - ).distinct() + return queryset.filter(variants__discount__gte=1).distinct() def filter_by_size(self, queryset, name, value): - return queryset.filter( - variants__variant="Size", - variants__value__iexact=value - ).distinct() + return queryset.filter(variants__variant="Size", variants__value__iexact=value).distinct() def filter_by_color(self, queryset, name, value): - return queryset.filter( - variants__variant="Color", - variants__value__iexact=value - ).distinct() - - def filter_queryset(self, queryset): - variant_real_price_expr = Case( - When(discount=-1, then=F("price")), - When( - discount__gte=0, - then=ExpressionWrapper( - F("price") - (F("price") * F("discount") / 100), - output_field=FloatField() - ) - ), - output_field=FloatField() - ) - - cheapest_variant_qs = ( - AdVariant.objects - .filter(ad=OuterRef("pk")) - .annotate(real_price=variant_real_price_expr) - .order_by("real_price") - .values("real_price")[:1] - ) - - ad_real_price = F("price") - - queryset = queryset.annotate( - real_price=Case( - When( - variants__isnull=False, - then=Subquery(cheapest_variant_qs) - ), - default=ad_real_price, - output_field=FloatField() - ) - ).distinct() - - return super().filter_queryset(queryset) + return queryset.filter(variants__variant="Color", variants__value__iexact=value).distinct() diff --git a/core/apps/api/filters/category.py b/core/apps/api/filters/category.py index 732c333..f792d50 100644 --- a/core/apps/api/filters/category.py +++ b/core/apps/api/filters/category.py @@ -1,13 +1,13 @@ from django_filters import rest_framework as filters -from core.apps.api.models import Category +from core.apps.api.models import CategoryModel class CategoryFilter(filters.FilterSet): # name = filters.CharFilter(field_name="name", lookup_expr="icontains") class Meta: - model = Category + model = CategoryModel fields = [ "show_home", "id", diff --git a/core/apps/api/management/__init__.py b/core/apps/api/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/api/management/commands/__init__.py b/core/apps/api/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/api/management/commands/create_fake_data.py b/core/apps/api/management/commands/create_fake_data.py new file mode 100644 index 0000000..d307e23 --- /dev/null +++ b/core/apps/api/management/commands/create_fake_data.py @@ -0,0 +1,400 @@ +import random +from decimal import Decimal +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model +from django.db import transaction +from core.apps.api.models import ( + CategoryModel, + AdModel, + AdImageModel, + AdVariantModel, + AdOptionModel, + TagsModel, + ColorModel, + BannerModel, + FeedbackModel, + OrderModel, + OrderItemModel, + AdTopPlanModel, +) +from core.apps.api.choices import AdType, AdCategoryType, AdVariantType, OrderStatus + +User = get_user_model() + + +class Command(BaseCommand): + help = 'Create fake data for testing API endpoints' + + def add_arguments(self, parser): + parser.add_argument( + '--users', + type=int, + default=5, + help='Number of users to create' + ) + parser.add_argument( + '--ads', + type=int, + default=20, + help='Number of ads to create' + ) + parser.add_argument( + '--clear', + action='store_true', + help='Clear existing data before creating new' + ) + + def handle(self, *args, **options): + users_count = options['users'] + ads_count = options['ads'] + clear_data = options['clear'] + + if clear_data: + self.stdout.write(self.style.WARNING('Clearing existing data...')) + self.clear_data() + + self.stdout.write(self.style.SUCCESS('Starting fake data generation...')) + + with transaction.atomic(): + # Create basic data + plan = self.create_plan() + colors = self.create_colors() + tags = self.create_tags() + categories = self.create_categories() + users = self.create_users(users_count) + + # Create banners + banners = self.create_banners(categories[:3]) + + # Create ads with related data + ads = self.create_ads(ads_count, users, categories, plan, tags, colors) + + # Create feedbacks + feedbacks = self.create_feedbacks(ads, users) + + # Create orders + orders = self.create_orders(ads, users) + + self.stdout.write(self.style.SUCCESS('\n' + '='*50)) + self.stdout.write(self.style.SUCCESS('Fake data created successfully!')) + self.stdout.write(self.style.SUCCESS('='*50)) + self.stdout.write(f'Users: {len(users)}') + self.stdout.write(f'Categories: {len(categories)}') + self.stdout.write(f'Tags: {len(tags)}') + self.stdout.write(f'Colors: {len(colors)}') + self.stdout.write(f'Ads: {len(ads)}') + self.stdout.write(f'Feedbacks: {len(feedbacks)}') + self.stdout.write(f'Orders: {len(orders)}') + self.stdout.write(f'Banners: {len(banners)}') + + def clear_data(self): + """Clear all existing data""" + models = [ + OrderItemModel, + OrderModel, + FeedbackModel, + AdImageModel, + AdVariantModel, + AdOptionModel, + AdModel, + BannerModel, + CategoryModel, + TagsModel, + ColorModel, + ] + + for model in models: + count = model.objects.all().delete()[0] + self.stdout.write(f'Deleted {count} {model.__name__} objects') + + def create_plan(self): + """Create or get default plan""" + plan, created = AdTopPlanModel.objects.get_or_create( + name="Free", + defaults={ + 'price': Decimal('0.00'), + 'duration_days': 30, + 'description': 'Free basic plan' + } + ) + if created: + self.stdout.write(self.style.SUCCESS('✓ Created default plan')) + return plan + + def create_colors(self): + """Create colors""" + colors_data = [ + 'Red', 'Blue', 'Green', 'Black', 'White', + 'Yellow', 'Orange', 'Purple', 'Pink', 'Brown' + ] + colors = [] + for color_name in colors_data: + color, created = ColorModel.objects.get_or_create(name=color_name) + colors.append(color) + + self.stdout.write(self.style.SUCCESS(f'✓ Created {len(colors)} colors')) + return colors + + def create_tags(self): + """Create tags""" + tags_data = [ + 'New', 'Hot', 'Sale', 'Popular', 'Featured', + 'Limited', 'Exclusive', 'Premium', 'Budget', 'Trending' + ] + tags = [] + for tag_name in tags_data: + tag, created = TagsModel.objects.get_or_create( + name=tag_name, + defaults={'slug': tag_name.lower()} + ) + tags.append(tag) + + self.stdout.write(self.style.SUCCESS(f'✓ Created {len(tags)} tags')) + return tags + + def create_categories(self): + """Create categories""" + categories_data = [ + {'name': 'Electronics', 'show_home': True}, + {'name': 'Fashion', 'show_home': True}, + {'name': 'Home & Garden', 'show_home': True}, + {'name': 'Sports', 'show_home': False}, + {'name': 'Toys', 'show_home': False}, + {'name': 'Books', 'show_home': True}, + {'name': 'Automotive', 'show_home': False}, + {'name': 'Beauty', 'show_home': True}, + ] + + categories = [] + for cat_data in categories_data: + cat, created = CategoryModel.objects.get_or_create( + name=cat_data['name'], + defaults={ + 'show_home': cat_data['show_home'], + 'category_type': AdCategoryType.PRODUCT, + 'level': 0 + } + ) + categories.append(cat) + + self.stdout.write(self.style.SUCCESS(f'✓ Created {len(categories)} categories')) + return categories + + def create_users(self, count): + """Create test users""" + users = [] + + # Create admin user + admin, created = User.objects.get_or_create( + username='admin', + defaults={ + 'email': 'admin@example.com', + 'is_staff': True, + 'is_superuser': True, + } + ) + if created: + admin.set_password('admin123') + admin.save() + users.append(admin) + + # Create regular users + for i in range(1, count): + user, created = User.objects.get_or_create( + username=f'user{i}', + defaults={ + 'email': f'user{i}@example.com', + 'first_name': f'User', + 'last_name': f'{i}', + } + ) + if created: + user.set_password('password123') + user.save() + users.append(user) + + self.stdout.write(self.style.SUCCESS(f'✓ Created {len(users)} users')) + return users + + def create_banners(self, categories): + """Create banners""" + banners = [] + banner_data = [ + { + 'title': 'Summer Sale', + 'description': 'Up to 50% off on selected items', + 'bg_color': '#FF5733', + 'text_color': '#FFFFFF', + }, + { + 'title': 'New Arrivals', + 'description': 'Check out our latest products', + 'bg_color': '#3498DB', + 'text_color': '#FFFFFF', + }, + { + 'title': 'Free Shipping', + 'description': 'On orders over $50', + 'bg_color': '#2ECC71', + 'text_color': '#FFFFFF', + }, + ] + + for i, data in enumerate(banner_data): + banner = BannerModel.objects.create( + title=data['title'], + description=data['description'], + link=f'/category/{categories[i].id}/' if i < len(categories) else '/', + bg_color=data['bg_color'], + text_color=data['text_color'], + order=i, + is_active=True, + ) + banners.append(banner) + + self.stdout.write(self.style.SUCCESS(f'✓ Created {len(banners)} banners')) + return banners + + def create_ads(self, count, users, categories, plan, tags, colors): + """Create ads with variants, images, and options""" + ads = [] + + products = [ + 'Smartphone', 'Laptop', 'T-Shirt', 'Jeans', 'Sofa', + 'Chair', 'Football', 'Basketball', 'Book', 'Watch', + 'Headphones', 'Camera', 'Shoes', 'Bag', 'Sunglasses', + 'Jacket', 'Perfume', 'Toy Car', 'Bicycle', 'Tablet' + ] + + for i in range(count): + product_name = products[i % len(products)] + category = random.choice(categories) + user = random.choice(users) + + ad = AdModel.objects.create( + user=user, + name=f'{product_name} - Model {i+1}', + ad_type=random.choice([AdType.SALE, AdType.RENT]), + category=category, + ad_category_type=AdCategoryType.PRODUCT, + _price=Decimal(random.randint(10, 1000)), + discount=Decimal(random.randint(0, 30)), + is_available=True, + physical_product=True, + plan=plan, + description=f'High quality {product_name.lower()} with amazing features. ' + f'Perfect condition, great price. Limited stock available!', + ) + + # Add random tags + ad.tags.set(random.sample(tags, random.randint(1, 3))) + + # Create variants + for color in random.sample(colors, random.randint(1, 3)): + AdVariantModel.objects.create( + ad=ad, + variant=AdVariantType.COLOR, + value=color.name, + color=color, + price=Decimal(random.randint(50, 500)), + stock_quantity=random.randint(1, 100), + is_available=True, + ) + + # Create size variants + sizes = ['S', 'M', 'L', 'XL'] + for size in random.sample(sizes, random.randint(1, 3)): + AdVariantModel.objects.create( + ad=ad, + variant=AdVariantType.SIZE, + value=size, + price=Decimal(random.randint(50, 500)), + stock_quantity=random.randint(1, 100), + is_available=True, + ) + + # Create options + options = [ + ('Brand', random.choice(['Samsung', 'Apple', 'Sony', 'LG', 'Generic'])), + ('Warranty', f'{random.choice([6, 12, 24])} months'), + ('Condition', random.choice(['New', 'Like New', 'Used - Good'])), + ] + for opt_name, opt_value in options: + AdOptionModel.objects.create( + ad=ad, + name=opt_name, + value=opt_value, + ) + + ads.append(ad) + + self.stdout.write(self.style.SUCCESS(f'✓ Created {len(ads)} ads with variants and options')) + return ads + + def create_feedbacks(self, ads, users): + """Create feedbacks for ads""" + feedbacks = [] + + comments = [ + 'Great product! Highly recommended.', + 'Good quality, fast delivery.', + 'Exactly as described. Very satisfied.', + 'Amazing! Worth every penny.', + 'Nice product but delivery was slow.', + 'Excellent quality and service.', + 'Very good, will buy again.', + 'Perfect condition, thank you!', + 'Good value for money.', + 'Satisfied with the purchase.', + ] + + for ad in random.sample(ads, min(len(ads), 15)): + for _ in range(random.randint(1, 5)): + user = random.choice(users) + feedback = FeedbackModel.objects.create( + user=user, + ad=ad, + star=random.randint(3, 5), + comment=random.choice(comments), + ) + feedbacks.append(feedback) + + self.stdout.write(self.style.SUCCESS(f'✓ Created {len(feedbacks)} feedbacks')) + return feedbacks + + def create_orders(self, ads, users): + """Create orders""" + orders = [] + + for user in random.sample(users, min(len(users), 3)): + for _ in range(random.randint(1, 3)): + order = OrderModel.objects.create( + user=user, + status=random.choice([ + OrderStatus.PENDING, + OrderStatus.PROCESSING, + OrderStatus.COMPLETED + ]), + total_amount=Decimal('0.00'), + ) + + # Add order items + total = Decimal('0.00') + for ad in random.sample(ads, random.randint(1, 3)): + quantity = random.randint(1, 3) + price = ad.price + + OrderItemModel.objects.create( + order=order, + ad=ad, + price=price, + quantity=quantity, + ) + total += price * quantity + + order.total_amount = total + order.save() + orders.append(order) + + self.stdout.write(self.style.SUCCESS(f'✓ Created {len(orders)} orders')) + return orders diff --git a/core/apps/api/migrations/0001_initial.py b/core/apps/api/migrations/0001_initial.py index 837b5dd..d0a83d0 100644 --- a/core/apps/api/migrations/0001_initial.py +++ b/core/apps/api/migrations/0001_initial.py @@ -1,5 +1,8 @@ -# Generated by Django 5.2.7 on 2025-11-22 11:39 +# Generated by Django 5.2.7 on 2025-12-06 15:57 +import django.core.validators +import django.db.models.deletion +from django.conf import settings from django.db import migrations, models @@ -8,27 +11,330 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('accounts', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Banner', + name='AdTopPlanModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255, unique=True, verbose_name='Plan Name')), + ('price', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Price')), + ('duration', models.PositiveIntegerField(verbose_name='Duration (days)')), + ('description', models.TextField(blank=True, verbose_name='Description')), + ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), + ], + options={ + 'verbose_name': 'Ad Top Plan', + 'verbose_name_plural': 'Ad Top Plans', + 'db_table': 'ad_top_plan', + 'ordering': ['price'], + }, + ), + migrations.CreateModel( + name='BannerModel', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), ('title', models.CharField(max_length=255, verbose_name='Title')), ('description', models.TextField(verbose_name='Description')), - ('mobile_image', models.ImageField(upload_to='banner/mobile_image/', verbose_name='Mobile Image')), - ('desktop_image', models.ImageField(upload_to='banner/desktop_image/', verbose_name='Desktop Image')), + ('mobile_image', models.ImageField(upload_to='banners/mobile/', verbose_name='Mobile Image')), + ('desktop_image', models.ImageField(upload_to='banners/desktop/', verbose_name='Desktop Image')), ('link', models.URLField(verbose_name='Link')), - ('bg_color', models.CharField(max_length=255, verbose_name='BG Color')), - ('text_color', models.CharField(max_length=255, verbose_name='Text Color')), + ('bg_color', models.CharField(default='#FFFFFF', max_length=7, verbose_name='Background Color')), + ('text_color', models.CharField(default='#000000', max_length=7, verbose_name='Text Color')), + ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), + ('order', models.PositiveIntegerField(default=0, verbose_name='Display Order')), ], options={ 'verbose_name': 'Banner', 'verbose_name_plural': 'Banners', 'db_table': 'banner', + 'ordering': ['order', '-created_at'], }, ), + migrations.CreateModel( + name='ColorModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255, unique=True, verbose_name='Name')), + ], + options={ + 'verbose_name': 'Color', + 'verbose_name_plural': 'Colors', + 'db_table': 'color', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='TagsModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255, unique=True, verbose_name='Tag Name')), + ('slug', models.SlugField(max_length=255, unique=True, verbose_name='Slug')), + ], + options={ + 'verbose_name': 'Tag', + 'verbose_name_plural': 'Tags', + 'db_table': 'tags', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='AdModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('ad_type', models.CharField(choices=[('Buy', 'Buy'), ('Sell', 'Sell')], max_length=255, verbose_name='Type')), + ('ad_category_type', models.CharField(choices=[('Product', 'Product'), ('Service', 'Service'), ('Auto', 'Auto'), ('Home', 'Home')], max_length=255, verbose_name='Category Type')), + ('_price', models.DecimalField(blank=True, db_column='price', decimal_places=2, max_digits=10, null=True, verbose_name='Price')), + ('discount', models.DecimalField(decimal_places=2, default=-1, max_digits=10, verbose_name='Discount')), + ('is_available', models.BooleanField(default=True, verbose_name='Is available')), + ('physical_product', models.BooleanField(default=False, verbose_name='Physical product')), + ('image', models.ImageField(upload_to='ads/', verbose_name='Image')), + ('description', models.TextField(verbose_name='Description')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ads', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.adtopplanmodel', verbose_name='Plan')), + ], + options={ + 'verbose_name': 'Ad', + 'verbose_name_plural': 'Ads', + 'db_table': 'ad', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='AdSizeModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('weight', models.PositiveIntegerField(verbose_name='Weight (g)')), + ('width', models.PositiveIntegerField(verbose_name='Width (cm)')), + ('height', models.PositiveIntegerField(verbose_name='Height (cm)')), + ('length', models.PositiveIntegerField(verbose_name='Length (cm)')), + ('ad', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='size_info', to='api.admodel', verbose_name='Ad')), + ], + options={ + 'verbose_name': 'Ad Size', + 'verbose_name_plural': 'Ad Sizes', + 'db_table': 'ad_size', + }, + ), + migrations.CreateModel( + name='AdVariantModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('value', models.CharField(max_length=255, verbose_name='Value')), + ('is_available', models.BooleanField(default=True, verbose_name='Is Available')), + ('price', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Price')), + ('stock_quantity', models.PositiveIntegerField(default=0, verbose_name='Stock Quantity')), + ('ad', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variants', to='api.admodel', verbose_name='Ad')), + ('color', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.colormodel', verbose_name='Color')), + ], + options={ + 'verbose_name': 'Ad Variant', + 'verbose_name_plural': 'Ad Variants', + 'db_table': 'ad_variant', + }, + ), + migrations.CreateModel( + name='CategoryModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255, verbose_name='Category Name')), + ('name_uz', models.CharField(max_length=255, null=True, verbose_name='Category Name')), + ('name_ru', models.CharField(max_length=255, null=True, verbose_name='Category Name')), + ('name_en', models.CharField(max_length=255, null=True, verbose_name='Category Name')), + ('show_home', models.BooleanField(default=False, verbose_name='Show on Home')), + ('level', models.IntegerField(default=0, editable=False, verbose_name='Level')), + ('image', models.ImageField(blank=True, null=True, upload_to='categories/', verbose_name='Image')), + ('category_type', models.CharField(choices=[('Product', 'Product'), ('Service', 'Service'), ('Auto', 'Auto'), ('Home', 'Home')], default='Product', max_length=255, verbose_name='Category Type')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='api.categorymodel', verbose_name='Parent Category')), + ], + options={ + 'verbose_name': 'Category', + 'verbose_name_plural': 'Categories', + 'db_table': 'category', + 'ordering': ['level', 'name'], + }, + ), + migrations.AddField( + model_name='admodel', + name='category', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.categorymodel', verbose_name='Category'), + ), + migrations.CreateModel( + name='FeedbackModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('star', models.IntegerField(default=5, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)], verbose_name='Rating')), + ('comment', models.TextField(max_length=1000, verbose_name='Comment')), + ('ad', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='api.admodel', verbose_name='Ad')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'Feedback', + 'verbose_name_plural': 'Feedbacks', + 'db_table': 'feedback', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='FeedbackImageModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('image', models.ImageField(upload_to='feedbacks/', verbose_name='Image')), + ('feedback', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='api.feedbackmodel', verbose_name='Feedback')), + ], + options={ + 'verbose_name': 'Feedback Image', + 'verbose_name_plural': 'Feedback Images', + 'db_table': 'feedback_images', + }, + ), + migrations.CreateModel( + name='OrderModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('status', models.CharField(choices=[('Pending', 'Pending'), ('Cancel', 'Cancel'), ('Done', 'Done')], db_index=True, max_length=255, verbose_name='Status')), + ('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Total Amount')), + ('address', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='accounts.address', verbose_name='Address')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'Order', + 'verbose_name_plural': 'Orders', + 'db_table': 'order', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='OrderItemModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Price')), + ('quantity', models.PositiveIntegerField(default=1, verbose_name='Quantity')), + ('ad', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='api.admodel', verbose_name='Ad')), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='api.ordermodel', verbose_name='Order')), + ], + options={ + 'verbose_name': 'Order Item', + 'verbose_name_plural': 'Order Items', + 'db_table': 'order_item', + }, + ), + migrations.AddField( + model_name='admodel', + name='tags', + field=models.ManyToManyField(blank=True, to='api.tagsmodel', verbose_name='Tags'), + ), + migrations.CreateModel( + name='AdOptionModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('value', models.CharField(max_length=255, verbose_name='Value')), + ('ad', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='api.admodel', verbose_name='Ad')), + ], + options={ + 'verbose_name': 'Ad Option', + 'verbose_name_plural': 'Ad Options', + 'db_table': 'ad_option', + 'unique_together': {('ad', 'name')}, + }, + ), + migrations.CreateModel( + name='AdImageModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('image', models.ImageField(upload_to='ads/images/', verbose_name='Image')), + ('order', models.PositiveIntegerField(default=0, verbose_name='Display Order')), + ('is_primary', models.BooleanField(default=False, verbose_name='Is Primary')), + ('ad', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='api.admodel', verbose_name='Ad')), + ('ad_variant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='images', to='api.advariantmodel', verbose_name='Ad Variant')), + ], + options={ + 'verbose_name': 'Ad Image', + 'verbose_name_plural': 'Ad Images', + 'db_table': 'ad_images', + 'ordering': ['order', '-created_at'], + 'indexes': [models.Index(fields=['ad', 'is_primary'], name='ad_images_ad_id_ed2cb8_idx')], + }, + ), + migrations.AddIndex( + model_name='categorymodel', + index=models.Index(fields=['parent', 'show_home'], name='category_parent__ebe06e_idx'), + ), + migrations.AddIndex( + model_name='categorymodel', + index=models.Index(fields=['level'], name='category_level_e67701_idx'), + ), + migrations.AddIndex( + model_name='advariantmodel', + index=models.Index(fields=['ad', 'is_available'], name='ad_variant_ad_id_3a5d5e_idx'), + ), + migrations.AlterUniqueTogether( + name='advariantmodel', + unique_together={('ad', 'value')}, + ), + migrations.AddIndex( + model_name='feedbackmodel', + index=models.Index(fields=['ad', '-created_at'], name='feedback_ad_id_444d80_idx'), + ), + migrations.AddIndex( + model_name='feedbackmodel', + index=models.Index(fields=['user'], name='feedback_user_id_8cf53b_idx'), + ), + migrations.AlterUniqueTogether( + name='feedbackmodel', + unique_together={('user', 'ad')}, + ), + migrations.AddIndex( + model_name='ordermodel', + index=models.Index(fields=['user', '-created_at'], name='order_user_id_13e363_idx'), + ), + migrations.AddIndex( + model_name='ordermodel', + index=models.Index(fields=['status'], name='order_status_35c31c_idx'), + ), + migrations.AlterUniqueTogether( + name='orderitemmodel', + unique_together={('order', 'ad')}, + ), + migrations.AddIndex( + model_name='admodel', + index=models.Index(fields=['-created_at'], name='ad_created_359de0_idx'), + ), + migrations.AddIndex( + model_name='admodel', + index=models.Index(fields=['category', 'is_available'], name='ad_categor_8ff346_idx'), + ), ] diff --git a/core/apps/api/migrations/0002_adtopplan_color_tags_admodel_adimage_adoption_adsize_and_more.py b/core/apps/api/migrations/0002_adtopplan_color_tags_admodel_adimage_adoption_adsize_and_more.py deleted file mode 100644 index 22a739e..0000000 --- a/core/apps/api/migrations/0002_adtopplan_color_tags_admodel_adimage_adoption_adsize_and_more.py +++ /dev/null @@ -1,240 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-24 06:45 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0004_business_searchhistory'), - ('api', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='AdTopPlan', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=255, verbose_name='Name')), - ('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Price')), - ('duration', models.IntegerField(verbose_name='Duration')), - ], - options={ - 'verbose_name': 'AdTop Plan', - 'verbose_name_plural': 'AdTop Plan', - 'db_table': 'ad_top_plan', - }, - ), - migrations.CreateModel( - name='Color', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=255, verbose_name='Name')), - ], - options={ - 'verbose_name': 'Color', - 'verbose_name_plural': 'Colors', - 'db_table': 'color', - }, - ), - migrations.CreateModel( - name='Tags', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=255, verbose_name='Name')), - ], - options={ - 'verbose_name': 'Tags', - 'verbose_name_plural': 'Tags', - 'db_table': 'tags', - }, - ), - migrations.CreateModel( - name='AdModel', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=255, verbose_name='Name')), - ('ad_type', models.CharField(choices=[('Buy', 'Buy'), ('Sell', 'Sell')], max_length=255, verbose_name='Type')), - ('ad_category_type', models.CharField(choices=[('Product', 'Product'), ('Service', 'Service'), ('Auto', 'Auto'), ('Home', 'Home')], max_length=255, verbose_name='Type')), - ('price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Price')), - ('is_available', models.BooleanField(blank=True, default=True, null=True, verbose_name='Is available')), - ('physical_product', models.BooleanField(default=False, verbose_name='Physical product')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ad', to=settings.AUTH_USER_MODEL, verbose_name='User')), - ('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.adtopplan', verbose_name='Plan')), - ], - options={ - 'verbose_name': 'Ad', - 'verbose_name_plural': 'Ads', - 'db_table': 'ad', - }, - ), - migrations.CreateModel( - name='AdImage', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('image', models.ImageField(upload_to='ads/images/', verbose_name='Image')), - ('ad', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.admodel', verbose_name='Ad')), - ], - options={ - 'verbose_name': 'Ad_Image', - 'verbose_name_plural': 'Ad_Images', - 'db_table': 'ad_images', - }, - ), - migrations.CreateModel( - name='AdOption', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=255, verbose_name='Name')), - ('value', models.CharField(max_length=255, verbose_name='Value')), - ('ad', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.admodel')), - ], - options={ - 'verbose_name': 'Ad_Option', - 'verbose_name_plural': 'Ad_Options', - 'db_table': 'ad_option', - }, - ), - migrations.CreateModel( - name='AdSize', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('weight', models.PositiveIntegerField(verbose_name='Weight')), - ('width', models.PositiveIntegerField(verbose_name='Width')), - ('height', models.PositiveIntegerField(verbose_name='Height')), - ('length', models.PositiveIntegerField(verbose_name='Length')), - ('ad', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.admodel')), - ], - options={ - 'verbose_name': 'AdSize', - 'verbose_name_plural': 'AdSizes', - 'db_table': 'ad_size', - }, - ), - migrations.CreateModel( - name='AdVariant', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('variant', models.CharField(choices=[('Color', 'Color'), ('Size', 'Size')], db_index=True, max_length=255)), - ('value', models.CharField(max_length=255)), - ('is_available', models.CharField(max_length=255)), - ('price', models.DecimalField(decimal_places=2, max_digits=10)), - ('discount', models.DecimalField(decimal_places=2, default=-1, max_digits=10)), - ('ad', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.admodel')), - ], - options={ - 'verbose_name': 'Ad_Variant', - 'verbose_name_plural': 'Ad_Variants', - 'db_table': 'ad_variant', - }, - ), - migrations.CreateModel( - name='Category', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=255, verbose_name='Category Name')), - ('show_home', models.BooleanField(default=False, verbose_name='Show Home')), - ('level', models.IntegerField(default=0, verbose_name='Level')), - ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='api.category')), - ], - options={ - 'verbose_name': 'Category', - 'verbose_name_plural': 'Categories', - 'db_table': 'category', - }, - ), - migrations.AddField( - model_name='admodel', - name='category', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.category', verbose_name='Category'), - ), - migrations.CreateModel( - name='Feedback', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('star', models.IntegerField(default=0, verbose_name='Star')), - ('command', models.CharField(max_length=255, verbose_name='Command')), - ('ad', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.admodel', verbose_name='Ad')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')), - ], - options={ - 'verbose_name': 'Feedback', - 'verbose_name_plural': 'Feedbacks', - 'db_table': 'feedback', - }, - ), - migrations.CreateModel( - name='FeedbackImages', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('image', models.ImageField(upload_to='feedback/images/', verbose_name='Image')), - ('feedback', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.feedback', verbose_name='Feedback')), - ], - options={ - 'verbose_name': 'Feedback Images', - 'verbose_name_plural': 'Feedback Images', - 'db_table': 'feedback_images', - }, - ), - migrations.CreateModel( - name='Order', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('status', models.CharField(choices=[('Pending', 'Pending'), ('Cancel', 'Cancel'), ('Done', 'Done')], max_length=255)), - ('address', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.address', verbose_name='Address')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')), - ], - options={ - 'verbose_name': 'Order', - 'verbose_name_plural': 'Orders', - 'db_table': 'order', - }, - ), - migrations.CreateModel( - name='OrderItem', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Price')), - ('count', models.PositiveIntegerField(default=0, verbose_name='Count')), - ('ad', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.admodel', verbose_name='Ad')), - ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.order', verbose_name='Order')), - ], - options={ - 'verbose_name': 'Order Item', - 'verbose_name_plural': 'Order Items', - 'db_table': 'order_item', - }, - ), - migrations.AddField( - model_name='admodel', - name='tags', - field=models.ManyToManyField(to='api.tags', verbose_name='Tags'), - ), - ] diff --git a/core/apps/api/migrations/0002_sizemodel_alter_advariantmodel_unique_together_and_more.py b/core/apps/api/migrations/0002_sizemodel_alter_advariantmodel_unique_together_and_more.py new file mode 100644 index 0000000..cab69cf --- /dev/null +++ b/core/apps/api/migrations/0002_sizemodel_alter_advariantmodel_unique_together_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 5.2.7 on 2025-12-06 16:11 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='SizeModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=100, unique=True, verbose_name='Name')), + ], + options={ + 'verbose_name': 'Size', + 'verbose_name_plural': 'Sizes', + 'db_table': 'size', + 'ordering': ['name'], + }, + ), + migrations.AlterUniqueTogether( + name='advariantmodel', + unique_together=set(), + ), + migrations.AlterField( + model_name='advariantmodel', + name='color', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.colormodel', verbose_name='Color'), + ), + migrations.AddField( + model_name='advariantmodel', + name='size', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.sizemodel', verbose_name='Size'), + ), + migrations.RemoveField( + model_name='advariantmodel', + name='value', + ), + ] diff --git a/core/apps/api/migrations/0003_category_image.py b/core/apps/api/migrations/0003_category_image.py deleted file mode 100644 index 1d24a56..0000000 --- a/core/apps/api/migrations/0003_category_image.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-24 10:15 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0002_adtopplan_color_tags_admodel_adimage_adoption_adsize_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='category', - name='image', - field=models.ImageField(blank=True, null=True, upload_to='', verbose_name='Image'), - ), - ] diff --git a/core/apps/api/migrations/0003_colormodel_color.py b/core/apps/api/migrations/0003_colormodel_color.py new file mode 100644 index 0000000..ee8bb13 --- /dev/null +++ b/core/apps/api/migrations/0003_colormodel_color.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-12-06 16:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0002_sizemodel_alter_advariantmodel_unique_together_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='colormodel', + name='color', + field=models.CharField(max_length=255, null=True, verbose_name='Color'), + ), + ] diff --git a/core/apps/api/migrations/0004_category_category_type.py b/core/apps/api/migrations/0004_category_category_type.py deleted file mode 100644 index 1bb0995..0000000 --- a/core/apps/api/migrations/0004_category_category_type.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-25 07:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0003_category_image'), - ] - - operations = [ - migrations.AddField( - model_name='category', - name='category_type', - field=models.CharField(choices=[('Product', 'Product'), ('Service', 'Service'), ('Auto', 'Auto'), ('Home', 'Home')], default='Product', max_length=255, verbose_name='Category Type'), - ), - ] diff --git a/core/apps/api/migrations/0005_admodel_star.py b/core/apps/api/migrations/0005_admodel_star.py deleted file mode 100644 index bff48f1..0000000 --- a/core/apps/api/migrations/0005_admodel_star.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-25 10:10 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0004_category_category_type'), - ] - - operations = [ - migrations.AddField( - model_name='admodel', - name='star', - field=models.FloatField(default=0.0, verbose_name='Star'), - ), - ] diff --git a/core/apps/api/migrations/0006_alter_adimage_ad.py b/core/apps/api/migrations/0006_alter_adimage_ad.py deleted file mode 100644 index b4fa955..0000000 --- a/core/apps/api/migrations/0006_alter_adimage_ad.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-25 10:30 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0005_admodel_star'), - ] - - operations = [ - migrations.AlterField( - model_name='adimage', - name='ad', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='api.admodel', verbose_name='Ad'), - ), - ] diff --git a/core/apps/api/migrations/0007_alter_advariant_ad.py b/core/apps/api/migrations/0007_alter_advariant_ad.py deleted file mode 100644 index e98da00..0000000 --- a/core/apps/api/migrations/0007_alter_advariant_ad.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-25 10:35 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0006_alter_adimage_ad'), - ] - - operations = [ - migrations.AlterField( - model_name='advariant', - name='ad', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variants', to='api.admodel'), - ), - ] diff --git a/core/apps/api/migrations/0008_adimage_ad_variant.py b/core/apps/api/migrations/0008_adimage_ad_variant.py deleted file mode 100644 index f66b8f5..0000000 --- a/core/apps/api/migrations/0008_adimage_ad_variant.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-25 10:43 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0007_alter_advariant_ad'), - ] - - operations = [ - migrations.AddField( - model_name='adimage', - name='ad_variant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.advariant', verbose_name='Ad Variant'), - ), - ] diff --git a/core/apps/api/migrations/0009_alter_adimage_ad_variant.py b/core/apps/api/migrations/0009_alter_adimage_ad_variant.py deleted file mode 100644 index ff64c1c..0000000 --- a/core/apps/api/migrations/0009_alter_adimage_ad_variant.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-25 10:45 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0008_adimage_ad_variant'), - ] - - operations = [ - migrations.AlterField( - model_name='adimage', - name='ad_variant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='images', to='api.advariant', verbose_name='Ad Variant'), - ), - ] diff --git a/core/apps/api/migrations/0010_remove_admodel_star_admodel_image.py b/core/apps/api/migrations/0010_remove_admodel_star_admodel_image.py deleted file mode 100644 index 77658f9..0000000 --- a/core/apps/api/migrations/0010_remove_admodel_star_admodel_image.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-25 11:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0009_alter_adimage_ad_variant'), - ] - - operations = [ - migrations.RemoveField( - model_name='admodel', - name='star', - ), - migrations.AddField( - model_name='admodel', - name='image', - field=models.ImageField(default=1, upload_to='', verbose_name='Image'), - preserve_default=False, - ), - ] diff --git a/core/apps/api/migrations/0011_alter_feedback_ad.py b/core/apps/api/migrations/0011_alter_feedback_ad.py deleted file mode 100644 index bf9f1c9..0000000 --- a/core/apps/api/migrations/0011_alter_feedback_ad.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-25 11:23 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0010_remove_admodel_star_admodel_image'), - ] - - operations = [ - migrations.AlterField( - model_name='feedback', - name='ad', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedback', to='api.admodel', verbose_name='Ad'), - ), - ] diff --git a/core/apps/api/migrations/0012_rename_command_feedback_comment.py b/core/apps/api/migrations/0012_rename_command_feedback_comment.py deleted file mode 100644 index 55fe778..0000000 --- a/core/apps/api/migrations/0012_rename_command_feedback_comment.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-25 11:38 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0011_alter_feedback_ad'), - ] - - operations = [ - migrations.RenameField( - model_name='feedback', - old_name='command', - new_name='comment', - ), - ] diff --git a/core/apps/api/migrations/0013_alter_feedback_comment.py b/core/apps/api/migrations/0013_alter_feedback_comment.py deleted file mode 100644 index be838a6..0000000 --- a/core/apps/api/migrations/0013_alter_feedback_comment.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-26 10:04 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0012_rename_command_feedback_comment'), - ] - - operations = [ - migrations.AlterField( - model_name='feedback', - name='comment', - field=models.CharField(max_length=255, verbose_name='Comment'), - ), - ] diff --git a/core/apps/api/migrations/0014_admodel_description.py b/core/apps/api/migrations/0014_admodel_description.py deleted file mode 100644 index b2ebbe7..0000000 --- a/core/apps/api/migrations/0014_admodel_description.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-28 11:21 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0013_alter_feedback_comment'), - ] - - operations = [ - migrations.AddField( - model_name='admodel', - name='description', - field=models.TextField(default=1, verbose_name='Description'), - preserve_default=False, - ), - ] diff --git a/core/apps/api/migrations/0014_category_name_en_category_name_ru_category_name_uz.py b/core/apps/api/migrations/0014_category_name_en_category_name_ru_category_name_uz.py deleted file mode 100644 index 142d2f3..0000000 --- a/core/apps/api/migrations/0014_category_name_en_category_name_ru_category_name_uz.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-27 07:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0013_alter_feedback_comment'), - ] - - operations = [ - migrations.AddField( - model_name='category', - name='name_en', - field=models.CharField(max_length=255, null=True, verbose_name='Category Name'), - ), - migrations.AddField( - model_name='category', - name='name_ru', - field=models.CharField(max_length=255, null=True, verbose_name='Category Name'), - ), - migrations.AddField( - model_name='category', - name='name_uz', - field=models.CharField(max_length=255, null=True, verbose_name='Category Name'), - ), - ] diff --git a/core/apps/api/migrations/0015_alter_adoption_ad.py b/core/apps/api/migrations/0015_alter_adoption_ad.py deleted file mode 100644 index 00675db..0000000 --- a/core/apps/api/migrations/0015_alter_adoption_ad.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-28 11:45 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0014_admodel_description'), - ] - - operations = [ - migrations.AlterField( - model_name='adoption', - name='ad', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='api.admodel', verbose_name='Ad'), - ), - ] diff --git a/core/apps/api/migrations/0016_merge_20251202_1732.py b/core/apps/api/migrations/0016_merge_20251202_1732.py deleted file mode 100644 index 5e037c3..0000000 --- a/core/apps/api/migrations/0016_merge_20251202_1732.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 5.2.7 on 2025-12-02 12:32 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0014_category_name_en_category_name_ru_category_name_uz'), - ('api', '0015_alter_adoption_ad'), - ] - - operations = [ - ] diff --git a/core/apps/api/models/__init__.py b/core/apps/api/models/__init__.py index c7d32cb..bcf28b1 100644 --- a/core/apps/api/models/__init__.py +++ b/core/apps/api/models/__init__.py @@ -1,5 +1,5 @@ -from .banner import * # noqa -from .feedback import * # noqa -from .ad import * # noqa -from .ad_items import * # noqa -from .order import * # noqa +from .banner import * +from .feedback import * +from .ad import * +from .common import * +from .order import * diff --git a/core/apps/api/models/ad/__init__.py b/core/apps/api/models/ad/__init__.py index 8c2d0b4..34184d9 100644 --- a/core/apps/api/models/ad/__init__.py +++ b/core/apps/api/models/ad/__init__.py @@ -1,2 +1,6 @@ -from .ad import * # noqa -from .category import * # noqa +from .ad import * +from .category import * +from .variant import * +from .image import * +from .option import * +from .size import * diff --git a/core/apps/api/models/ad/ad.py b/core/apps/api/models/ad/ad.py index fb08480..09a2f91 100644 --- a/core/apps/api/models/ad/ad.py +++ b/core/apps/api/models/ad/ad.py @@ -1,45 +1,43 @@ +# type: ignore from django.db import models from django_core.models.base import AbstractBaseModel from django.utils.translation import gettext_lazy as _ from django.contrib.auth import get_user_model from core.apps.api.choices.ad_type import AdType, AdCategoryType -from model_bakery import baker class AdModel(AbstractBaseModel): - user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, verbose_name=_("User"), related_name="ad") - name = models.CharField(verbose_name=_("Name"), max_length=255) - ad_type = models.CharField(verbose_name=_("Type"), max_length=255, choices=AdType) - category = models.ForeignKey("api.Category", on_delete=models.CASCADE, verbose_name=_("Category")) - ad_category_type = models.CharField(verbose_name=_("Type"), max_length=255, choices=AdCategoryType) - price = models.DecimalField(verbose_name=_("Price"), max_digits=10, decimal_places=2, null=True, blank=True) - is_available = models.BooleanField(verbose_name=_("Is available"), default=True, blank=True, null=True) - physical_product = models.BooleanField(verbose_name=_("Physical product"), default=False) - plan = models.ForeignKey("api.AdTopPlan", on_delete=models.CASCADE, verbose_name=_("Plan")) - tags = models.ManyToManyField("api.Tags", verbose_name=_("Tags")) - image = models.ImageField(verbose_name=_("Image")) - description = models.TextField(verbose_name=_("Description")) - - @classmethod - def _baker(cls): - return baker.make(cls) + user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, verbose_name=_("User"), related_name="ads") + name = models.CharField(_("Name"), max_length=255) + ad_type = models.CharField(_("Type"), max_length=255, choices=AdType) + category = models.ForeignKey("api.CategoryModel", on_delete=models.CASCADE, verbose_name=_("Category")) + ad_category_type = models.CharField(_("Category Type"), max_length=255, choices=AdCategoryType) + _price = models.DecimalField(_("Price"), max_digits=10, decimal_places=2, null=True, blank=True, db_column="price") + discount = models.DecimalField(_("Discount"), max_digits=10, decimal_places=2, default=-1) + is_available = models.BooleanField(_("Is available"), default=True) + physical_product = models.BooleanField(_("Physical product"), default=False) + plan = models.ForeignKey("api.AdTopPlanModel", on_delete=models.CASCADE, verbose_name=_("Plan")) + tags = models.ManyToManyField("api.TagsModel", verbose_name=_("Tags"), blank=True) + image = models.ImageField(_("Image"), upload_to="ads/") + description = models.TextField(_("Description")) def __str__(self): - return str(self.pk) + return self.name + + @property + def price(self): + """Get actual price - either from variant or direct price""" + if self.ad_category_type == AdCategoryType.PRODUCT.value: + variant = self.variants.order_by("price").first() + return variant.price if variant else 0 + return self._price class Meta: db_table = "ad" verbose_name = _("Ad") verbose_name_plural = _("Ads") - - -class Color(AbstractBaseModel): - name = models.CharField(verbose_name=_("Name"), max_length=255) - - def __str__(self): - return str(self.pk) - - class Meta: - db_table = "color" - verbose_name = _("Color") - verbose_name_plural = _("Colors") + ordering = ["-created_at"] + indexes = [ + models.Index(fields=["-created_at"]), + models.Index(fields=["category", "is_available"]), + ] diff --git a/core/apps/api/models/ad/category.py b/core/apps/api/models/ad/category.py index 605349c..217f086 100644 --- a/core/apps/api/models/ad/category.py +++ b/core/apps/api/models/ad/category.py @@ -2,26 +2,45 @@ from django.db import models from django_core.models.base import AbstractBaseModel from django.utils.translation import gettext_lazy as _ from core.apps.api.choices import AdCategoryType -from model_bakery import baker -class Category(AbstractBaseModel): - name = models.CharField(max_length=255, verbose_name=_('Category Name')) - parent = models.ForeignKey('self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) - show_home = models.BooleanField(default=False, verbose_name=_('Show Home')) - level = models.IntegerField(default=0, verbose_name=_('Level')) - image = models.ImageField(verbose_name=_('Image'), null=True, blank=True) - category_type = models.CharField(max_length=255, verbose_name=_('Category Type'), choices=AdCategoryType, - default=AdCategoryType.PRODUCT) - - @classmethod - def _baker(cls): - return baker.make(cls) +class CategoryModel(AbstractBaseModel): + name = models.CharField(_("Category Name"), max_length=255) + parent = models.ForeignKey( + "self", + null=True, + blank=True, + related_name="children", + on_delete=models.CASCADE, + verbose_name=_("Parent Category") + ) + show_home = models.BooleanField(_("Show on Home"), default=False) + level = models.IntegerField(_("Level"), default=0, editable=False) + image = models.ImageField(_("Image"), upload_to="categories/", null=True, blank=True) + category_type = models.CharField( + _("Category Type"), + max_length=255, + choices=AdCategoryType, + default=AdCategoryType.PRODUCT + ) def __str__(self): - return str(self.pk) + return self.name + + def save(self, *args, **kwargs): + """Auto-calculate level based on parent""" + if self.parent: + self.level = self.parent.level + 1 + else: + self.level = 0 + super().save(*args, **kwargs) class Meta: - db_table = 'category' - verbose_name = _('Category') - verbose_name_plural = _('Categories') + db_table = "category" + verbose_name = _("Category") + verbose_name_plural = _("Categories") + ordering = ["level", "name"] + indexes = [ + models.Index(fields=["parent", "show_home"]), + models.Index(fields=["level"]), + ] diff --git a/core/apps/api/models/ad/image.py b/core/apps/api/models/ad/image.py new file mode 100644 index 0000000..067016c --- /dev/null +++ b/core/apps/api/models/ad/image.py @@ -0,0 +1,35 @@ +from django.db import models +from django_core.models.base import AbstractBaseModel +from django.utils.translation import gettext_lazy as _ + + +class AdImageModel(AbstractBaseModel): + ad = models.ForeignKey( + "api.AdModel", + on_delete=models.CASCADE, + verbose_name=_("Ad"), + related_name="images" + ) + ad_variant = models.ForeignKey( + "api.AdVariantModel", + on_delete=models.CASCADE, + verbose_name=_("Ad Variant"), + related_name="images", + null=True, + blank=True + ) + image = models.ImageField(_("Image"), upload_to="ads/images/") + order = models.PositiveIntegerField(_("Display Order"), default=0) + is_primary = models.BooleanField(_("Is Primary"), default=False) + + def __str__(self): + return f"Image for {self.ad.name}" + + class Meta: + db_table = "ad_images" + verbose_name = _("Ad Image") + verbose_name_plural = _("Ad Images") + ordering = ["order", "-created_at"] + indexes = [ + models.Index(fields=["ad", "is_primary"]), + ] diff --git a/core/apps/api/models/ad/option.py b/core/apps/api/models/ad/option.py new file mode 100644 index 0000000..b7e7968 --- /dev/null +++ b/core/apps/api/models/ad/option.py @@ -0,0 +1,23 @@ +from django.db import models +from django_core.models.base import AbstractBaseModel +from django.utils.translation import gettext_lazy as _ + + +class AdOptionModel(AbstractBaseModel): + ad = models.ForeignKey( + "api.AdModel", + on_delete=models.CASCADE, + related_name="options", + verbose_name=_("Ad") + ) + name = models.CharField(_("Name"), max_length=255) + value = models.CharField(_("Value"), max_length=255) + + def __str__(self): + return f"{self.name}: {self.value}" + + class Meta: + db_table = "ad_option" + verbose_name = _("Ad Option") + verbose_name_plural = _("Ad Options") + unique_together = [["ad", "name"]] diff --git a/core/apps/api/models/ad/size.py b/core/apps/api/models/ad/size.py new file mode 100644 index 0000000..0c08803 --- /dev/null +++ b/core/apps/api/models/ad/size.py @@ -0,0 +1,24 @@ +from django.db import models +from django_core.models.base import AbstractBaseModel +from django.utils.translation import gettext_lazy as _ + + +class AdSizeModel(AbstractBaseModel): + ad = models.ForeignKey( + "api.AdModel", + on_delete=models.CASCADE, + related_name="size_info", + verbose_name=_("Ad") + ) + weight = models.PositiveIntegerField(_("Weight (g)")) + width = models.PositiveIntegerField(_("Width (cm)")) + height = models.PositiveIntegerField(_("Height (cm)")) + length = models.PositiveIntegerField(_("Length (cm)")) + + def __str__(self): + return f"{self.width}x{self.height}x{self.length}cm, {self.weight}g" + + class Meta: + db_table = "ad_size" + verbose_name = _("Ad Size") + verbose_name_plural = _("Ad Sizes") diff --git a/core/apps/api/models/ad/variant.py b/core/apps/api/models/ad/variant.py new file mode 100644 index 0000000..a0caaaf --- /dev/null +++ b/core/apps/api/models/ad/variant.py @@ -0,0 +1,26 @@ +from django.db import models +from django_core.models.base import AbstractBaseModel +from django.utils.translation import gettext_lazy as _ +from django.core.validators import MinValueValidator + + +class AdVariantModel(AbstractBaseModel): + ad = models.ForeignKey("api.AdModel", on_delete=models.CASCADE, related_name="variants", verbose_name=_("Ad")) + color = models.ForeignKey( + "api.ColorModel", on_delete=models.CASCADE, verbose_name=_("Color"), null=True, blank=False + ) + size = models.ForeignKey("api.SizeModel", on_delete=models.CASCADE, verbose_name=_("Size"), null=True, blank=False) + is_available = models.BooleanField(_("Is Available"), default=True) + price = models.DecimalField(_("Price"), max_digits=10, decimal_places=2, validators=[MinValueValidator(0)]) + stock_quantity = models.PositiveIntegerField(_("Stock Quantity"), default=0) + + def __str__(self): + return f"{self.color} - {self.size}" + + class Meta: + db_table = "ad_variant" + verbose_name = _("Ad Variant") + verbose_name_plural = _("Ad Variants") + indexes = [ + models.Index(fields=["ad", "is_available"]), + ] diff --git a/core/apps/api/models/ad_items/__init__.py b/core/apps/api/models/ad_items/__init__.py deleted file mode 100644 index b44fb2d..0000000 --- a/core/apps/api/models/ad_items/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .ad_top_plan import * # noqa -from .tags import * # noqa -from .ad_images import * # noqa -from .ad_option import * # noqa -from .ad_size import * # noqa -from .ad_variant import * # noqa diff --git a/core/apps/api/models/ad_items/ad_images.py b/core/apps/api/models/ad_items/ad_images.py deleted file mode 100644 index 49c19d3..0000000 --- a/core/apps/api/models/ad_items/ad_images.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.db import models -from django_core.models.base import AbstractBaseModel -from django.utils.translation import gettext_lazy as _ - -from core.apps.api.models import AdModel - - -class AdImage(AbstractBaseModel): - image = models.ImageField(verbose_name=_("Image"), upload_to="ads/images/") - ad = models.ForeignKey(AdModel, verbose_name=_("Ad"), related_name="images", - on_delete=models.CASCADE) - ad_variant = models.ForeignKey("api.AdVariant", verbose_name=_("Ad Variant"), null=True, blank=True, - related_name="images", - on_delete=models.CASCADE) - - def __str__(self): - return str(self.pk) - - class Meta: - db_table = "ad_images" - verbose_name = _("Ad_Image") - verbose_name_plural = _("Ad_Images") diff --git a/core/apps/api/models/ad_items/ad_option.py b/core/apps/api/models/ad_items/ad_option.py deleted file mode 100644 index 5aa449c..0000000 --- a/core/apps/api/models/ad_items/ad_option.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.db import models -from django_core.models.base import AbstractBaseModel -from django.utils.translation import gettext_lazy as _ -from core.apps.api.models import AdModel - - -class AdOption(AbstractBaseModel): - name = models.CharField(_("Name"), max_length=255) - value = models.CharField(_("Value"), max_length=255) - ad = models.ForeignKey(AdModel, on_delete=models.CASCADE, related_name="options", verbose_name=_("Ad")) - - def __str__(self): - return str(self.pk) - - class Meta: - db_table = "ad_option" - verbose_name = _("Ad_Option") - verbose_name_plural = _("Ad_Options") diff --git a/core/apps/api/models/ad_items/ad_size.py b/core/apps/api/models/ad_items/ad_size.py deleted file mode 100644 index 8d8a1b2..0000000 --- a/core/apps/api/models/ad_items/ad_size.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.db import models -from django_core.models.base import AbstractBaseModel -from django.utils.translation import gettext_lazy as _ -from core.apps.api.models import AdModel - - -class AdSize(AbstractBaseModel): - ad = models.ForeignKey(AdModel, on_delete=models.CASCADE) - weight = models.PositiveIntegerField(verbose_name=_("Weight")) - width = models.PositiveIntegerField(verbose_name=_("Width")) - height = models.PositiveIntegerField(verbose_name=_("Height")) - length = models.PositiveIntegerField(verbose_name=_("Length")) - - def __str__(self): - return str(self.pk) - - class Meta: - db_table = "ad_size" - verbose_name = _("AdSize") - verbose_name_plural = _("AdSizes") diff --git a/core/apps/api/models/ad_items/ad_top_plan.py b/core/apps/api/models/ad_items/ad_top_plan.py deleted file mode 100644 index bfd4f3a..0000000 --- a/core/apps/api/models/ad_items/ad_top_plan.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.db import models -from django_core.models.base import AbstractBaseModel -from django.utils.translation import gettext_lazy as _ - - -class AdTopPlan(AbstractBaseModel): - name = models.CharField(verbose_name=_('Name'), max_length=255) - price = models.DecimalField(verbose_name=_('Price'), max_digits=10, decimal_places=2) - duration = models.IntegerField(verbose_name=_('Duration')) - - def __str__(self): - return str(self.pk) - class Meta: - db_table = 'ad_top_plan' - verbose_name = _('AdTop Plan') - verbose_name_plural = _('AdTop Plan') diff --git a/core/apps/api/models/ad_items/ad_variant.py b/core/apps/api/models/ad_items/ad_variant.py deleted file mode 100644 index a67f961..0000000 --- a/core/apps/api/models/ad_items/ad_variant.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.db import models -from django_core.models.base import AbstractBaseModel -from django.utils.translation import gettext_lazy as _ -from core.apps.api.models import AdModel -from core.apps.api.choices.ad_variant_type import AdVariantType - - -class AdVariant(AbstractBaseModel): - ad = models.ForeignKey(AdModel, on_delete=models.CASCADE, related_name="variants") - variant = models.CharField(max_length=255, choices=AdVariantType, db_index=True) - value = models.CharField(max_length=255) - is_available = models.CharField(max_length=255) - price = models.DecimalField(max_digits=10, decimal_places=2) - discount = models.DecimalField(max_digits=10, decimal_places=2, default=-1) - - def __str__(self): - return str(self.pk) - - class Meta: - db_table = "ad_variant" - verbose_name = _("Ad_Variant") - verbose_name_plural = _("Ad_Variants") diff --git a/core/apps/api/models/ad_items/tags.py b/core/apps/api/models/ad_items/tags.py deleted file mode 100644 index f51285e..0000000 --- a/core/apps/api/models/ad_items/tags.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.db import models -from django_core.models.base import AbstractBaseModel -from django.utils.translation import gettext_lazy as _ - - -class Tags(AbstractBaseModel): - name = models.CharField(verbose_name=_("Name"), max_length=255) - - def __str__(self): - return str(self.pk) - - class Meta: - db_table = 'tags' - verbose_name = _("Tags") - verbose_name_plural = _("Tags") diff --git a/core/apps/api/models/banner/__init__.py b/core/apps/api/models/banner/__init__.py index 1b144fd..7490662 100644 --- a/core/apps/api/models/banner/__init__.py +++ b/core/apps/api/models/banner/__init__.py @@ -1 +1 @@ -from .banner import * # noqa +from .banner import * diff --git a/core/apps/api/models/banner/banner.py b/core/apps/api/models/banner/banner.py index e2babe0..5083ad5 100644 --- a/core/apps/api/models/banner/banner.py +++ b/core/apps/api/models/banner/banner.py @@ -1,26 +1,30 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from django_core.models.base import AbstractBaseModel -from model_bakery import baker -class Banner(AbstractBaseModel): - title = models.CharField(max_length=255, verbose_name=_("Title")) - description = models.TextField(verbose_name=_("Description")) - mobile_image = models.ImageField(verbose_name=_("Mobile Image"), upload_to="banner/mobile_image/") - desktop_image = models.ImageField(verbose_name=_("Desktop Image"), upload_to="banner/desktop_image/") - link = models.URLField(verbose_name=_("Link")) - bg_color = models.CharField(verbose_name=_("BG Color"), max_length=255) - text_color = models.CharField(verbose_name=_("Text Color"), max_length=255) - - @classmethod - def _baker(cls): - return baker.make(cls) +class BannerModel(AbstractBaseModel): + title = models.CharField(_("Title"), max_length=255) + description = models.TextField(_("Description")) + mobile_image = models.ImageField( + _("Mobile Image"), + upload_to="banners/mobile/" + ) + desktop_image = models.ImageField( + _("Desktop Image"), + upload_to="banners/desktop/" + ) + link = models.URLField(_("Link")) + bg_color = models.CharField(_("Background Color"), max_length=7, default="#FFFFFF") + text_color = models.CharField(_("Text Color"), max_length=7, default="#000000") + is_active = models.BooleanField(_("Is Active"), default=True) + order = models.PositiveIntegerField(_("Display Order"), default=0) def __str__(self): - return str(self.pk) + return self.title class Meta: db_table = "banner" verbose_name = _("Banner") verbose_name_plural = _("Banners") + ordering = ["order", "-created_at"] diff --git a/core/apps/api/models/common/__init__.py b/core/apps/api/models/common/__init__.py new file mode 100644 index 0000000..e8506b6 --- /dev/null +++ b/core/apps/api/models/common/__init__.py @@ -0,0 +1,4 @@ +from .tags import * +from .plan import * +from .size import * # noqa +from .color import * # noqa diff --git a/core/apps/api/models/common/color.py b/core/apps/api/models/common/color.py new file mode 100644 index 0000000..5c9dff8 --- /dev/null +++ b/core/apps/api/models/common/color.py @@ -0,0 +1,17 @@ +from django.db import models +from django_core.models.base import AbstractBaseModel +from django.utils.translation import gettext_lazy as _ + + +class ColorModel(AbstractBaseModel): + name = models.CharField(_("Name"), max_length=255, unique=True) + color = models.CharField(_("Color"), max_length=255, null=True, blank=False) + + def __str__(self): + return self.name + + class Meta: + db_table = "color" + verbose_name = _("Color") + verbose_name_plural = _("Colors") + ordering = ["name"] diff --git a/core/apps/api/models/common/plan.py b/core/apps/api/models/common/plan.py new file mode 100644 index 0000000..615065c --- /dev/null +++ b/core/apps/api/models/common/plan.py @@ -0,0 +1,26 @@ +from django.db import models +from django_core.models.base import AbstractBaseModel +from django.utils.translation import gettext_lazy as _ +from django.core.validators import MinValueValidator + + +class AdTopPlanModel(AbstractBaseModel): + name = models.CharField(_("Plan Name"), max_length=255, unique=True) + price = models.DecimalField( + _("Price"), + max_digits=10, + decimal_places=2, + validators=[MinValueValidator(0)] + ) + duration = models.PositiveIntegerField(_("Duration (days)")) + description = models.TextField(_("Description"), blank=True) + is_active = models.BooleanField(_("Is Active"), default=True) + + def __str__(self): + return f"{self.name} - {self.duration} days" + + class Meta: + db_table = "ad_top_plan" + verbose_name = _("Ad Top Plan") + verbose_name_plural = _("Ad Top Plans") + ordering = ["price"] diff --git a/core/apps/api/models/common/size.py b/core/apps/api/models/common/size.py new file mode 100644 index 0000000..cd89a1c --- /dev/null +++ b/core/apps/api/models/common/size.py @@ -0,0 +1,16 @@ +from django.db import models +from django_core.models.base import AbstractBaseModel +from django.utils.translation import gettext_lazy as _ + + +class SizeModel(AbstractBaseModel): + name = models.CharField(_("Name"), max_length=100, unique=True) + + def __str__(self): + return self.name + + class Meta: + db_table = "size" + verbose_name = _("Size") + verbose_name_plural = _("Sizes") + ordering = ["name"] diff --git a/core/apps/api/models/common/tags.py b/core/apps/api/models/common/tags.py new file mode 100644 index 0000000..9ad869b --- /dev/null +++ b/core/apps/api/models/common/tags.py @@ -0,0 +1,17 @@ +from django.db import models +from django_core.models.base import AbstractBaseModel +from django.utils.translation import gettext_lazy as _ + + +class TagsModel(AbstractBaseModel): + name = models.CharField(_("Tag Name"), max_length=255, unique=True) + slug = models.SlugField(_("Slug"), max_length=255, unique=True) + + def __str__(self): + return self.name + + class Meta: + db_table = "tags" + verbose_name = _("Tag") + verbose_name_plural = _("Tags") + ordering = ["name"] diff --git a/core/apps/api/models/feedback/__init__.py b/core/apps/api/models/feedback/__init__.py index 5e1a07c..c2ed394 100644 --- a/core/apps/api/models/feedback/__init__.py +++ b/core/apps/api/models/feedback/__init__.py @@ -1 +1 @@ -from .feedback import * # noqa +from .feedback import * diff --git a/core/apps/api/models/feedback/feedback.py b/core/apps/api/models/feedback/feedback.py index c8ba25e..ef4bdd9 100644 --- a/core/apps/api/models/feedback/feedback.py +++ b/core/apps/api/models/feedback/feedback.py @@ -2,33 +2,57 @@ from django.db import models from django_core.models.base import AbstractBaseModel from django.utils.translation import gettext_lazy as _ from django.contrib.auth import get_user_model -from core.apps.api.models.ad import AdModel +from django.core.validators import MinValueValidator, MaxValueValidator -class Feedback(AbstractBaseModel): - star = models.IntegerField(default=0, verbose_name=_("Star")) - user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, verbose_name=_("User")) - ad = models.ForeignKey(AdModel, on_delete=models.CASCADE, verbose_name=_("Ad"), related_name="feedback") - comment = models.CharField(max_length=255, verbose_name=_("Comment")) +class FeedbackModel(AbstractBaseModel): + star = models.IntegerField( + _("Rating"), + default=5, + validators=[MinValueValidator(1), MaxValueValidator(5)] + ) + user = models.ForeignKey( + get_user_model(), + on_delete=models.CASCADE, + verbose_name=_("User"), + related_name="feedbacks" + ) + ad = models.ForeignKey( + "api.AdModel", + on_delete=models.CASCADE, + verbose_name=_("Ad"), + related_name="feedbacks" + ) + comment = models.TextField(_("Comment"), max_length=1000) def __str__(self): - return str(self.pk) + return f"{self.user.username} - {self.ad.name} ({self.star}★)" class Meta: db_table = "feedback" verbose_name = _("Feedback") verbose_name_plural = _("Feedbacks") + ordering = ["-created_at"] + unique_together = [["user", "ad"]] + indexes = [ + models.Index(fields=["ad", "-created_at"]), + models.Index(fields=["user"]), + ] -class FeedbackImages(AbstractBaseModel): - feedback = models.ForeignKey(Feedback, on_delete=models.CASCADE, verbose_name=_("Feedback")) - image = models.ImageField(verbose_name=_("Image"), upload_to="feedback/" - "images/") +class FeedbackImageModel(AbstractBaseModel): + feedback = models.ForeignKey( + FeedbackModel, + on_delete=models.CASCADE, + verbose_name=_("Feedback"), + related_name="images" + ) + image = models.ImageField(_("Image"), upload_to="feedbacks/") def __str__(self): - return str(self.pk) + return f"Image for {self.feedback}" class Meta: db_table = "feedback_images" - verbose_name = _("Feedback Images") + verbose_name = _("Feedback Image") verbose_name_plural = _("Feedback Images") diff --git a/core/apps/api/models/order/__init__.py b/core/apps/api/models/order/__init__.py index 0747015..4ca2c09 100644 --- a/core/apps/api/models/order/__init__.py +++ b/core/apps/api/models/order/__init__.py @@ -1 +1 @@ -from .order import * # noqa +from .order import * diff --git a/core/apps/api/models/order/order.py b/core/apps/api/models/order/order.py index c2a8bb4..5043d4c 100644 --- a/core/apps/api/models/order/order.py +++ b/core/apps/api/models/order/order.py @@ -4,33 +4,82 @@ from django.utils.translation import gettext_lazy as _ from django.contrib.auth import get_user_model from core.apps.api.choices import OrderStatus from core.apps.accounts.models import Address -from core.apps.api.models import AdModel -class Order(AbstractBaseModel): - user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, verbose_name=_("User")) - status = models.CharField(max_length=255, choices=OrderStatus) - address = models.ForeignKey(Address, on_delete=models.CASCADE, verbose_name=_("Address")) +class OrderModel(AbstractBaseModel): + user = models.ForeignKey( + get_user_model(), + on_delete=models.CASCADE, + verbose_name=_("User"), + related_name="orders" + ) + status = models.CharField( + _("Status"), + max_length=255, + choices=OrderStatus, + db_index=True + ) + address = models.ForeignKey( + Address, + on_delete=models.PROTECT, + verbose_name=_("Address") + ) + total_amount = models.DecimalField( + _("Total Amount"), + max_digits=10, + decimal_places=2, + default=0 + ) def __str__(self): - return str(self.pk) + return f"Order #{self.pk} - {self.user.username}" + + def calculate_total(self): + """Calculate total from order items""" + total = sum(item.subtotal for item in self.items.all()) + self.total_amount = total + return total class Meta: db_table = "order" verbose_name = _("Order") verbose_name_plural = _("Orders") + ordering = ["-created_at"] + indexes = [ + models.Index(fields=["user", "-created_at"]), + models.Index(fields=["status"]), + ] -class OrderItem(models.Model): - order = models.ForeignKey(Order, on_delete=models.CASCADE, verbose_name=_("Order")) - price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name=_("Price")) - ad = models.ForeignKey(AdModel, on_delete=models.CASCADE, verbose_name=_("Ad")) - count = models.PositiveIntegerField(default=0, verbose_name=_("Count")) +class OrderItemModel(AbstractBaseModel): + order = models.ForeignKey( + OrderModel, + on_delete=models.CASCADE, + verbose_name=_("Order"), + related_name="items" + ) + ad = models.ForeignKey( + "api.AdModel", + on_delete=models.PROTECT, + verbose_name=_("Ad") + ) + price = models.DecimalField( + _("Price"), + max_digits=10, + decimal_places=2 + ) + quantity = models.PositiveIntegerField(_("Quantity"), default=1) def __str__(self): - return str(self.pk) + return f"{self.ad.name} x {self.quantity}" + + @property + def subtotal(self): + """Calculate item subtotal""" + return self.price * self.quantity class Meta: db_table = "order_item" verbose_name = _("Order Item") verbose_name_plural = _("Order Items") + unique_together = [["order", "ad"]] diff --git a/core/apps/api/serializers/__init__.py b/core/apps/api/serializers/__init__.py index 29847a6..d65e855 100644 --- a/core/apps/api/serializers/__init__.py +++ b/core/apps/api/serializers/__init__.py @@ -1,6 +1,7 @@ -from .category import * # noqa -from .search import * # noqa -from .ad import * # noqa -from .user import * # noqa -from .notification import * # noqa -from .banner import * # noqa +from .ad import * +from .banner import * +from .category import * +from .common import * # noqa +from .notification import * +from .search import * +from .user import * diff --git a/core/apps/api/serializers/ad/__init__.py b/core/apps/api/serializers/ad/__init__.py index 61a687b..27facc5 100644 --- a/core/apps/api/serializers/ad/__init__.py +++ b/core/apps/api/serializers/ad/__init__.py @@ -1,2 +1,2 @@ -from .home_api import * # noqa -from .ad import * # noqa +from .home_api import * +from .ad import * diff --git a/core/apps/api/serializers/ad/ad.py b/core/apps/api/serializers/ad/ad.py index 789f306..c5d468b 100644 --- a/core/apps/api/serializers/ad/ad.py +++ b/core/apps/api/serializers/ad/ad.py @@ -1,270 +1,148 @@ from rest_framework import serializers from django.db.models import Avg - from core.apps.accounts.choices import AccountType -from core.apps.api.models import AdModel, AdVariant, Category, AdImage, AdOption +from core.apps.api.models import AdModel, AdVariantModel, CategoryModel, AdImageModel, AdOptionModel from core.apps.accounts.models import UserLike from core.apps.api.choices import AdVariantType +from core.apps.api.serializers.common.color import ListColorSerializer +from core.apps.api.serializers.common.size import ListSizeSerializer class AdOptionSerializer(serializers.ModelSerializer): class Meta: - model = AdOption - fields = [ - "id", - "name", - "value", - ] + model = AdOptionModel + fields = ["id", "name", "value"] + read_only_fields = ["id"] class CategorySerializer(serializers.ModelSerializer): class Meta: - model = Category + model = CategoryModel fields = ["id", "name"] + read_only_fields = ["id"] class AdImageSerializer(serializers.ModelSerializer): class Meta: - model = AdImage - fields = [ - "image", - "ad_variant" - ] + model = AdImageModel + fields = ["id", "image", "ad_variant", "is_primary", "order"] + read_only_fields = ["id"] def to_representation(self, instance): data = super().to_representation(instance) - - if instance.ad_variant is None: + if not instance.ad_variant: data.pop("ad_variant", None) - return data class AdVariantSerializer(serializers.ModelSerializer): + color = ListColorSerializer() + size = ListSizeSerializer() + class Meta: - model = AdVariant - fields = [ - "id", - "variant", - "value", - "is_available", - "price", - "discount", - ] + model = AdVariantModel + fields = ["id", "size", "color", "is_available", "price", "stock_quantity"] + read_only_fields = ["id", "color_name"] -class BaseAdSerializer(serializers.ModelSerializer): +class AdListSerializer(serializers.ModelSerializer): is_liked = serializers.SerializerMethodField() - star = serializers.SerializerMethodField() - comment_count = serializers.SerializerMethodField() + rating = serializers.SerializerMethodField() + reviews_count = serializers.SerializerMethodField() class Meta: model = AdModel - fields = [ - "id", - "name", - "price", - "image", - "is_liked", - "star", - "comment_count", - ] + fields = ["id", "name", "price", "image", "discount", "is_liked", "rating", "reviews_count", "is_available"] + read_only_fields = fields - def get_star(self, obj): - avg = obj.feedback.aggregate(avg=Avg("star"))["avg"] - return avg or 0 + def get_rating(self, obj): + """Get average rating from feedbacks""" + avg = obj.feedbacks.aggregate(avg=Avg("star"))["avg"] + return round(avg, 1) if avg else 0 - def get_comment_count(self, obj): - count = obj.feedback.count() - return count or 0 + def get_reviews_count(self, obj): + """Get total count of feedbacks""" + return obj.feedbacks.count() def get_is_liked(self, obj): + """Check if current user liked this ad""" request = self.context.get("request") - user = getattr(request, "user", None) - - if not user or not user.is_authenticated: + if not request or not request.user.is_authenticated: return False - - return UserLike.objects.filter(user=user, ad=obj).exists() + return UserLike.objects.filter(user=request.user, ad=obj).exists() -class ListAdSerializer(BaseAdSerializer): - price = serializers.SerializerMethodField() - discount = serializers.SerializerMethodField() - - class Meta(BaseAdSerializer.Meta): - fields = [ - "id", - "name", - "price", - "image", - "star", - "comment_count", - "discount", - "is_liked", - ] - - def _get_first_variant(self, obj): - if not hasattr(self, "_variant_cache"): - self._variant_cache = {} - if obj.id not in self._variant_cache: - self._variant_cache[obj.id] = obj.variants.order_by("price").first() - return self._variant_cache[obj.id] - - def get_price(self, obj): - variant = self._get_first_variant(obj) - if not variant: - return obj.price - return variant.price if variant else 0 - - def get_discount(self, obj): - variant = self._get_first_variant(obj) - return variant.discount if variant else -1.0 - - -class FullListAdSerializer(serializers.Serializer): - ads = ListAdSerializer(many=True) - categories = serializers.SerializerMethodField() - colors = serializers.SerializerMethodField() - sizes = serializers.SerializerMethodField() - min_price = serializers.SerializerMethodField() - max_price = serializers.SerializerMethodField() - - def get_categories(self, obj): - ads = obj.get("ads", []) - - category_ids = set() - categories = [] - - for ad in ads: - category = ad.category - if category and category.id not in category_ids: - category_ids.add(category.id) - categories.append(category) - - return CategorySerializer(categories, many=True).data - - def get_colors(self, obj): - ads = obj.get("ads", []) - color_values = set() - - for ad in ads: - variants = getattr(ad, "variants", []) - for v in variants.all(): - if v.variant == AdVariantType.COLOR: - color_values.add(v.value) - - return list(color_values) - - def get_sizes(self, obj): - ads = obj.get("ads", []) - size_values = set() - - for ad in ads: - variants = getattr(ad, "variants", []) - for v in variants.all(): - if v.variant == AdVariantType.SIZE: - size_values.add(v.value) - - return list(size_values) - - def get_min_price(self, obj): - ads = obj.get("ads", []) - prices = [] - - for ad in ads: - ad_data = ListAdSerializer(ad, context=self.context).data - price = ad_data.get("price") - if price is not None: - prices.append(price) - - return min(prices) if prices else None - - def get_max_price(self, obj): - ads = obj.get("ads", []) - prices = [] - - for ad in ads: - ad_data = ListAdSerializer(ad, context=self.context).data - price = ad_data.get("price") - if price is not None: - prices.append(price) - - return max(prices) if prices else None - - -class RetrieveAdSerializer(BaseAdSerializer): +class AdDetailSerializer(AdListSerializer): variants = AdVariantSerializer(many=True, read_only=True) - images = serializers.SerializerMethodField() + images = AdImageSerializer(many=True, read_only=True) colors = serializers.SerializerMethodField() sizes = serializers.SerializerMethodField() creator = serializers.SerializerMethodField() options = AdOptionSerializer(many=True, read_only=True) + category = CategorySerializer(read_only=True) - class Meta(BaseAdSerializer.Meta): - fields = [ - "id", - "name", - "price", - "image", - "star", - "comment_count", - "is_liked", + class Meta(AdListSerializer.Meta): + fields = AdListSerializer.Meta.fields + [ + "description", "images", "variants", "colors", "sizes", "creator", - "description", - "options" + "options", + "category", + "ad_type", + "physical_product", ] - def get_images(self, obj): - objects = obj.images.all() - return AdImageSerializer(objects, many=True, context=self.context).data - def get_colors(self, obj): - color_values = set() - - variants = getattr(obj, "variants", []) - for v in variants.all(): - if v.variant == AdVariantType.COLOR: - color_values.add(v.value) - - return list(color_values) + """Get unique colors from variants""" + return list(obj.variants.values_list("color", flat=True).distinct()) def get_sizes(self, obj): - size_values = set() - - variants = getattr(obj, "variants", []) - for v in variants.all(): - if v.variant == AdVariantType.SIZE: - size_values.add(v.value) - return list(size_values) + """Get unique sizes from variants""" + return list(obj.variants.values_list("size", flat=True).distinct()) def get_creator(self, obj): + """Get creator information""" user = obj.user - user_type = user.account_type request = self.context.get("request") - avatar_url = request.build_absolute_uri(user.avatar.url) if user.avatar else None + avatar_url = None + if user.avatar and request: + avatar_url = request.build_absolute_uri(user.avatar.url) - if user_type == AccountType.BUSINESS: - return { - "username": user.business.name, - "avatar": avatar_url, - "create_at": user.validated_at, - "last_live": "endi qo'shamiz! waiting pls ))" - } + if user.account_type == AccountType.BUSINESS: + username = user.business.name if hasattr(user, "business") else user.username else: - username = f"{user.first_name} {user.last_name}" - return { - "username": username, - "avatar": avatar_url, - "create_at": user.validated_at, - "last_live": "endi qo'shamiz! waiting pls ))" - } + username = f"{user.first_name} {user.last_name}".strip() or user.username + + return { + "id": user.id, + "username": username, + "avatar": avatar_url, + "account_type": user.account_type, + "joined_at": user.date_joined, + } -class CreateAdSerializer(BaseAdSerializer): - class Meta(BaseAdSerializer.Meta): ... +class AdCreateSerializer(serializers.ModelSerializer): + class Meta: + model = AdModel + fields = [ + "name", + "ad_type", + "category", + "ad_category_type", + "discount", + "is_available", + "physical_product", + "plan", + "tags", + "image", + "description", + ] + + def create(self, validated_data): + validated_data["user"] = self.context["request"].user + return super().create(validated_data) diff --git a/core/apps/api/serializers/ad/home_api.py b/core/apps/api/serializers/ad/home_api.py index 3d10951..361c08e 100644 --- a/core/apps/api/serializers/ad/home_api.py +++ b/core/apps/api/serializers/ad/home_api.py @@ -1,26 +1,22 @@ from rest_framework import serializers from django.db.models import Avg -from core.apps.api.models import AdModel, AdVariant +from core.apps.api.models import AdModel, AdVariantModel from core.apps.accounts.models import UserLike class AdVariantSerializer(serializers.ModelSerializer): + color_name = serializers.CharField(source="color.name", read_only=True) + class Meta: - model = AdVariant - fields = [ - "variant", - "value", - "is_available", - "price", - "discount", - ] + model = AdVariantModel + fields = ["id", "value", "color_name", "is_available", "price"] + read_only_fields = fields -class BaseHomeAdSerializer(serializers.ModelSerializer): - star = serializers.SerializerMethodField() - comment_count = serializers.SerializerMethodField() - price = serializers.SerializerMethodField() - discount = serializers.SerializerMethodField() +class HomeAdListSerializer(serializers.ModelSerializer): + """Optimized serializer for home page ad listing""" + rating = serializers.SerializerMethodField() + reviews_count = serializers.SerializerMethodField() is_liked = serializers.SerializerMethodField() class Meta: @@ -30,54 +26,25 @@ class BaseHomeAdSerializer(serializers.ModelSerializer): "name", "price", "image", - "star", - "comment_count", + "rating", + "reviews_count", "discount", "is_liked", ] + read_only_fields = fields - def _get_first_variant(self, obj): - if not hasattr(self, "_variant_cache"): - self._variant_cache = {} - if obj.id not in self._variant_cache: - self._variant_cache[obj.id] = obj.variants.order_by("price").first() - return self._variant_cache[obj.id] + def get_rating(self, obj): + """Get average rating""" + avg = obj.feedbacks.aggregate(avg=Avg("star"))["avg"] + return round(avg, 1) if avg else 0 - def get_price(self, obj): - variant = self._get_first_variant(obj) - if not variant: - return obj.price - return variant.price if variant else 0 - - def get_discount(self, obj): - variant = self._get_first_variant(obj) - return variant.discount if variant else -1.0 - - def get_star(self, obj): - avg = obj.feedback.aggregate(avg=Avg("star"))["avg"] - return avg or 0 - - def get_comment_count(self, obj): - count = obj.feedback.count() - return count or 0 + def get_reviews_count(self, obj): + """Get feedback count""" + return obj.feedbacks.count() def get_is_liked(self, obj): + """Check if user liked this ad""" request = self.context.get("request") - user = getattr(request, "user", None) - - if not user or not user.is_authenticated: + if not request or not request.user.is_authenticated: return False - - return UserLike.objects.filter(user=user, ad=obj).exists() - - -class ListHomeAdSerializer(BaseHomeAdSerializer): - class Meta(BaseHomeAdSerializer.Meta): ... - - -class RetrieveHomeAdSerializer(BaseHomeAdSerializer): - class Meta(BaseHomeAdSerializer.Meta): ... - - -class CreateHomeAdSerializer(BaseHomeAdSerializer): - class Meta(BaseHomeAdSerializer.Meta): ... + return UserLike.objects.filter(user=request.user, ad=obj).exists() diff --git a/core/apps/api/serializers/banner/__init__.py b/core/apps/api/serializers/banner/__init__.py index 1b144fd..7490662 100644 --- a/core/apps/api/serializers/banner/__init__.py +++ b/core/apps/api/serializers/banner/__init__.py @@ -1 +1 @@ -from .banner import * # noqa +from .banner import * diff --git a/core/apps/api/serializers/banner/banner.py b/core/apps/api/serializers/banner/banner.py index cb37326..1098afb 100644 --- a/core/apps/api/serializers/banner/banner.py +++ b/core/apps/api/serializers/banner/banner.py @@ -1,11 +1,12 @@ from rest_framework import serializers -from core.apps.api.models import Banner +from core.apps.api.models import BannerModel class BaseBannerSerializer(serializers.ModelSerializer): class Meta: - model = Banner + model = BannerModel fields = [ + "id", "title", "description", "mobile_image", diff --git a/core/apps/api/serializers/category/__init__.py b/core/apps/api/serializers/category/__init__.py index d63c50f..63ecac4 100644 --- a/core/apps/api/serializers/category/__init__.py +++ b/core/apps/api/serializers/category/__init__.py @@ -1 +1 @@ -from .category import * # noqa +from .category import * diff --git a/core/apps/api/serializers/category/category.py b/core/apps/api/serializers/category/category.py index 0e652c4..fc8308a 100644 --- a/core/apps/api/serializers/category/category.py +++ b/core/apps/api/serializers/category/category.py @@ -1,13 +1,13 @@ from rest_framework import serializers -from core.apps.api.models import Category +from core.apps.api.models import CategoryModel class BaseCategorySerializer(serializers.ModelSerializer): children = serializers.SerializerMethodField() class Meta: - model = Category + model = CategoryModel fields = [ "id", "name", diff --git a/core/apps/api/serializers/common/__init__.py b/core/apps/api/serializers/common/__init__.py new file mode 100644 index 0000000..f0bbb6c --- /dev/null +++ b/core/apps/api/serializers/common/__init__.py @@ -0,0 +1,2 @@ +from .color import * # noqa +from .size import * # noqa diff --git a/core/apps/api/serializers/common/color.py b/core/apps/api/serializers/common/color.py new file mode 100644 index 0000000..c24bebf --- /dev/null +++ b/core/apps/api/serializers/common/color.py @@ -0,0 +1,29 @@ +from rest_framework import serializers + +from core.apps.api.models import ColorModel + + +class BaseColorSerializer(serializers.ModelSerializer): + class Meta: + model = ColorModel + fields = [ + "id", + "name", + "color", + ] + + +class ListColorSerializer(BaseColorSerializer): + class Meta(BaseColorSerializer.Meta): ... + + +class RetrieveColorSerializer(BaseColorSerializer): + class Meta(BaseColorSerializer.Meta): ... + + +class CreateColorSerializer(BaseColorSerializer): + class Meta(BaseColorSerializer.Meta): + fields = [ + "id", + "name", + ] diff --git a/core/apps/api/serializers/common/size.py b/core/apps/api/serializers/common/size.py new file mode 100644 index 0000000..996af86 --- /dev/null +++ b/core/apps/api/serializers/common/size.py @@ -0,0 +1,28 @@ +from rest_framework import serializers + +from core.apps.api.models import SizeModel + + +class BaseSizeSerializer(serializers.ModelSerializer): + class Meta: + model = SizeModel + fields = [ + "id", + "name", + ] + + +class ListSizeSerializer(BaseSizeSerializer): + class Meta(BaseSizeSerializer.Meta): ... + + +class RetrieveSizeSerializer(BaseSizeSerializer): + class Meta(BaseSizeSerializer.Meta): ... + + +class CreateSizeSerializer(BaseSizeSerializer): + class Meta(BaseSizeSerializer.Meta): + fields = [ + "id", + "name", + ] diff --git a/core/apps/api/serializers/feedback/__init__.py b/core/apps/api/serializers/feedback/__init__.py new file mode 100644 index 0000000..c2ed394 --- /dev/null +++ b/core/apps/api/serializers/feedback/__init__.py @@ -0,0 +1 @@ +from .feedback import * diff --git a/core/apps/api/serializers/feedback/feedback.py b/core/apps/api/serializers/feedback/feedback.py new file mode 100644 index 0000000..1542d95 --- /dev/null +++ b/core/apps/api/serializers/feedback/feedback.py @@ -0,0 +1,84 @@ +from rest_framework import serializers +from django.db.models import Avg +from core.apps.api.models import FeedbackModel, FeedbackImageModel, AdModel + + +class FeedbackImageSerializer(serializers.ModelSerializer): + class Meta: + model = FeedbackImageModel + fields = ["id", "image"] + read_only_fields = ["id"] + + +class FeedbackListSerializer(serializers.ModelSerializer): + """List serializer for feedback with user and ad info""" + user_name = serializers.CharField(source="user.username", read_only=True) + ad_name = serializers.CharField(source="ad.name", read_only=True) + images = FeedbackImageSerializer(many=True, read_only=True) + + class Meta: + model = FeedbackModel + fields = [ + "id", + "star", + "user", + "user_name", + "ad", + "ad_name", + "comment", + "images", + "created_at" + ] + read_only_fields = ["id", "user", "user_name", "ad_name", "created_at"] + + +class FeedbackDetailSerializer(FeedbackListSerializer): + """Detailed feedback serializer""" + class Meta(FeedbackListSerializer.Meta): + fields = FeedbackListSerializer.Meta.fields + ["updated_at"] + + +class FeedbackCreateSerializer(serializers.ModelSerializer): + """Create feedback serializer""" + images = serializers.ListField( + child=serializers.ImageField(), + write_only=True, + required=False + ) + + class Meta: + model = FeedbackModel + fields = ["ad", "star", "comment", "images"] + + def validate_star(self, value): + """Validate star rating is between 1 and 5""" + if not 1 <= value <= 5: + raise serializers.ValidationError("Rating must be between 1 and 5") + return value + + def create(self, validated_data): + images_data = validated_data.pop("images", []) + validated_data["user"] = self.context["request"].user + + feedback = FeedbackModel.objects.create(**validated_data) + + # Create feedback images + for image_data in images_data: + FeedbackImageModel.objects.create( + feedback=feedback, + image=image_data + ) + + return feedback + + +class FeedbackUpdateSerializer(serializers.ModelSerializer): + """Update feedback serializer""" + class Meta: + model = FeedbackModel + fields = ["star", "comment"] + + def validate_star(self, value): + if not 1 <= value <= 5: + raise serializers.ValidationError("Rating must be between 1 and 5") + return value diff --git a/core/apps/api/serializers/notification/__init__.py b/core/apps/api/serializers/notification/__init__.py index fc19dd2..1c164b2 100644 --- a/core/apps/api/serializers/notification/__init__.py +++ b/core/apps/api/serializers/notification/__init__.py @@ -1 +1 @@ -from .natification import * # noqa +from .notification import * diff --git a/core/apps/api/serializers/notification/natification.py b/core/apps/api/serializers/notification/notification.py similarity index 100% rename from core/apps/api/serializers/notification/natification.py rename to core/apps/api/serializers/notification/notification.py diff --git a/core/apps/api/serializers/order/__init__.py b/core/apps/api/serializers/order/__init__.py new file mode 100644 index 0000000..4ca2c09 --- /dev/null +++ b/core/apps/api/serializers/order/__init__.py @@ -0,0 +1 @@ +from .order import * diff --git a/core/apps/api/serializers/order/order.py b/core/apps/api/serializers/order/order.py new file mode 100644 index 0000000..4e20d8b --- /dev/null +++ b/core/apps/api/serializers/order/order.py @@ -0,0 +1,149 @@ +from rest_framework import serializers +from core.apps.api.models import OrderModel, OrderItemModel, AdModel +from core.apps.accounts.models import Address + + +class OrderItemSerializer(serializers.ModelSerializer): + """Order item serializer""" + ad_name = serializers.CharField(source="ad.name", read_only=True) + ad_image = serializers.ImageField(source="ad.image", read_only=True) + subtotal = serializers.DecimalField( + max_digits=10, + decimal_places=2, + read_only=True + ) + + class Meta: + model = OrderItemModel + fields = [ + "id", + "ad", + "ad_name", + "ad_image", + "price", + "quantity", + "subtotal" + ] + read_only_fields = ["id", "ad_name", "ad_image", "subtotal"] + + +class OrderListSerializer(serializers.ModelSerializer): + """List serializer for orders""" + items_count = serializers.SerializerMethodField() + status_display = serializers.CharField(source="get_status_display", read_only=True) + + class Meta: + model = OrderModel + fields = [ + "id", + "status", + "status_display", + "total_amount", + "items_count", + "created_at" + ] + read_only_fields = fields + + def get_items_count(self, obj): + return obj.items.count() + + +class OrderDetailSerializer(serializers.ModelSerializer): + """Detailed order serializer""" + items = OrderItemSerializer(many=True, read_only=True) + user_name = serializers.CharField(source="user.username", read_only=True) + address_details = serializers.SerializerMethodField() + status_display = serializers.CharField(source="get_status_display", read_only=True) + + class Meta: + model = OrderModel + fields = [ + "id", + "user", + "user_name", + "status", + "status_display", + "address", + "address_details", + "total_amount", + "items", + "created_at", + "updated_at" + ] + read_only_fields = [ + "id", "user", "user_name", "status_display", + "total_amount", "created_at", "updated_at" + ] + + def get_address_details(self, obj): + if obj.address: + return { + "id": obj.address.id, + "street": obj.address.street, + "city": obj.address.city, + "country": obj.address.country, + } + return None + + +class OrderItemCreateSerializer(serializers.Serializer): + """Serializer for creating order items""" + ad = serializers.PrimaryKeyRelatedField(queryset=AdModel.objects.all()) + quantity = serializers.IntegerField(min_value=1, default=1) + + +class OrderCreateSerializer(serializers.ModelSerializer): + """Create order serializer""" + items = OrderItemCreateSerializer(many=True, write_only=True) + + class Meta: + model = OrderModel + fields = ["address", "items"] + + def validate_items(self, value): + if not value: + raise serializers.ValidationError("Order must have at least one item") + return value + + def create(self, validated_data): + items_data = validated_data.pop("items") + validated_data["user"] = self.context["request"].user + + # Create order + order = OrderModel.objects.create(**validated_data) + + # Create order items and calculate total + total = 0 + for item_data in items_data: + ad = item_data["ad"] + quantity = item_data.get("quantity", 1) + price = ad.price + + OrderItemModel.objects.create( + order=order, + ad=ad, + price=price, + quantity=quantity + ) + total += price * quantity + + # Update order total + order.total_amount = total + order.save() + + return order + + +class OrderUpdateSerializer(serializers.ModelSerializer): + """Update order serializer""" + class Meta: + model = OrderModel + fields = ["status", "address"] + + def validate_status(self, value): + # Add business logic for status transitions + current_status = self.instance.status + # Example: Can't change completed orders + if current_status == "completed": + raise serializers.ValidationError("Cannot modify completed orders") + return value diff --git a/core/apps/api/serializers/search/__init__.py b/core/apps/api/serializers/search/__init__.py index 6287097..1d13162 100644 --- a/core/apps/api/serializers/search/__init__.py +++ b/core/apps/api/serializers/search/__init__.py @@ -1,2 +1,2 @@ -from .search import * # noqa -from .search_ads import * # noqa +from .history import * +from .ad import * diff --git a/core/apps/api/serializers/search/search_ads.py b/core/apps/api/serializers/search/ad.py similarity index 100% rename from core/apps/api/serializers/search/search_ads.py rename to core/apps/api/serializers/search/ad.py diff --git a/core/apps/api/serializers/search/search.py b/core/apps/api/serializers/search/history.py similarity index 100% rename from core/apps/api/serializers/search/search.py rename to core/apps/api/serializers/search/history.py diff --git a/core/apps/api/serializers/tags/__init__.py b/core/apps/api/serializers/tags/__init__.py new file mode 100644 index 0000000..11b89a3 --- /dev/null +++ b/core/apps/api/serializers/tags/__init__.py @@ -0,0 +1 @@ +from .tags import * diff --git a/core/apps/api/serializers/tags/tags.py b/core/apps/api/serializers/tags/tags.py new file mode 100644 index 0000000..b8b6e1c --- /dev/null +++ b/core/apps/api/serializers/tags/tags.py @@ -0,0 +1,43 @@ +from rest_framework import serializers +from django.utils.text import slugify +from core.apps.api.models import TagsModel, ColorModel + + +class TagSerializer(serializers.ModelSerializer): + """Serializer for tags""" + ads_count = serializers.SerializerMethodField() + + class Meta: + model = TagsModel + fields = ["id", "name", "slug", "ads_count"] + read_only_fields = ["id", "slug", "ads_count"] + + def get_ads_count(self, obj): + """Get count of ads using this tag""" + return obj.admodel_set.count() + + def create(self, validated_data): + # Auto-generate slug from name + if "slug" not in validated_data: + validated_data["slug"] = slugify(validated_data["name"]) + return super().create(validated_data) + + def update(self, instance, validated_data): + # Update slug if name changes + if "name" in validated_data: + validated_data["slug"] = slugify(validated_data["name"]) + return super().update(instance, validated_data) + + +class ColorSerializer(serializers.ModelSerializer): + """Serializer for colors""" + variants_count = serializers.SerializerMethodField() + + class Meta: + model = ColorModel + fields = ["id", "name", "variants_count"] + read_only_fields = ["id", "variants_count"] + + def get_variants_count(self, obj): + """Get count of variants using this color""" + return obj.advariantmodel_set.count() diff --git a/core/apps/api/serializers/user/__init__.py b/core/apps/api/serializers/user/__init__.py index 54d9396..f25faa8 100644 --- a/core/apps/api/serializers/user/__init__.py +++ b/core/apps/api/serializers/user/__init__.py @@ -1 +1 @@ -from .ad_like import * # noqa +from .like import * diff --git a/core/apps/api/serializers/user/ad_like.py b/core/apps/api/serializers/user/like.py similarity index 54% rename from core/apps/api/serializers/user/ad_like.py rename to core/apps/api/serializers/user/like.py index 13dc216..b79aefa 100644 --- a/core/apps/api/serializers/user/ad_like.py +++ b/core/apps/api/serializers/user/like.py @@ -1,13 +1,13 @@ from rest_framework import serializers from core.apps.accounts.models import UserLike from core.apps.api.models import AdModel -from core.apps.api.serializers.ad.home_api import ListHomeAdSerializer +from core.apps.api.serializers.ad.home_api import HomeAdListSerializer from rest_framework.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ class BaseUserLikeSerializer(serializers.ModelSerializer): - ad = ListHomeAdSerializer(many=False, read_only=True) + ad = HomeAdListSerializer(many=False, read_only=True) class Meta: model = UserLike @@ -28,18 +28,7 @@ class RetrieveUserLikeSerializer(BaseUserLikeSerializer): class CreateUserLikeSerializer(BaseUserLikeSerializer): ad = serializers.PrimaryKeyRelatedField(queryset=AdModel.objects.all()) - class Meta(BaseUserLikeSerializer.Meta): ... - - def validate(self, data): - user = self.context["request"].user - ad = data["ad"] - - if UserLike.objects.filter(user=user, ad=ad).exists(): - raise ValidationError({"detail": _("Siz bu e’longa allaqachon like bosgansiz.")}) - - return data - - def create(self, validated_data): - validated_data['user'] = self.context['request'].user - like = UserLike.objects.create(**validated_data) - return like + class Meta(BaseUserLikeSerializer.Meta): + fields = [ + "ad", + ] diff --git a/core/apps/api/tests/__init__.py b/core/apps/api/tests/__init__.py index c2b0648..de059db 100644 --- a/core/apps/api/tests/__init__.py +++ b/core/apps/api/tests/__init__.py @@ -1,5 +1,5 @@ -from .category import * # noqa -from .ad import * # noqa -from .search import * # noqa -from .user import * # noqa -from .banner import * # noqa +from .category import * +from .ad import * +from .search import * +from .user import * +from .banner import * diff --git a/core/apps/api/tests/ad/__init__.py b/core/apps/api/tests/ad/__init__.py index 49efb0e..d0b447f 100644 --- a/core/apps/api/tests/ad/__init__.py +++ b/core/apps/api/tests/ad/__init__.py @@ -1,2 +1,2 @@ -from .test_home_api import * # noqa -from .test_ad import * # noqa +from .test_home_api import * +from .test_ad import * diff --git a/core/apps/api/tests/ad/test_ad.py b/core/apps/api/tests/ad/test_ad.py index 7725462..fe9c8f9 100644 --- a/core/apps/api/tests/ad/test_ad.py +++ b/core/apps/api/tests/ad/test_ad.py @@ -13,7 +13,7 @@ def instance(db): @pytest.fixture def api_client(instance): client = APIClient() - ##client.force_authenticate(user=instance.user) + # client.force_authenticate(user=instance.user) return client, instance diff --git a/core/apps/api/tests/ad/test_home_api.py b/core/apps/api/tests/ad/test_home_api.py index 82f0a63..f28efbd 100644 --- a/core/apps/api/tests/ad/test_home_api.py +++ b/core/apps/api/tests/ad/test_home_api.py @@ -13,7 +13,7 @@ def instance(db): @pytest.fixture def api_client(instance): client = APIClient() - ##client.force_authenticate(user=instance.user) + # client.force_authenticate(user=instance.user) return client, instance diff --git a/core/apps/api/tests/banner/__init__.py b/core/apps/api/tests/banner/__init__.py index 0b9c46b..03c2638 100644 --- a/core/apps/api/tests/banner/__init__.py +++ b/core/apps/api/tests/banner/__init__.py @@ -1 +1 @@ -from .test_banner import * # noqa +from .test_banner import * diff --git a/core/apps/api/tests/banner/test_banner.py b/core/apps/api/tests/banner/test_banner.py index b71bad1..f1aa2e6 100644 --- a/core/apps/api/tests/banner/test_banner.py +++ b/core/apps/api/tests/banner/test_banner.py @@ -2,18 +2,18 @@ import pytest from django.urls import reverse from rest_framework.test import APIClient -from core.apps.api.models import Banner +from core.apps.api.models import BannerModel @pytest.fixture def instance(db): - return Banner._baker() + return BannerModel._baker() @pytest.fixture def api_client(instance): client = APIClient() - ## client.force_authenticate(user=instance.user) + # client.force_authenticate(user=instance.user) return client, instance diff --git a/core/apps/api/tests/category/__init__.py b/core/apps/api/tests/category/__init__.py index c7082d4..01f43c0 100644 --- a/core/apps/api/tests/category/__init__.py +++ b/core/apps/api/tests/category/__init__.py @@ -1 +1 @@ -from .test_category import * # noqa +from .test_category import * diff --git a/core/apps/api/tests/category/test_category.py b/core/apps/api/tests/category/test_category.py index e2c6abf..1574621 100644 --- a/core/apps/api/tests/category/test_category.py +++ b/core/apps/api/tests/category/test_category.py @@ -2,18 +2,18 @@ import pytest from django.urls import reverse from rest_framework.test import APIClient -from core.apps.api.models import Category +from core.apps.api.models import CategoryModel @pytest.fixture def instance(db): - return Category._baker() + return CategoryModel._baker() @pytest.fixture def api_client(instance): client = APIClient() - ##client.force_authenticate(user=instance.user) + # client.force_authenticate(user=instance.user) return client, instance diff --git a/core/apps/api/tests/search/__init__.py b/core/apps/api/tests/search/__init__.py index fd0bd42..d6d8438 100644 --- a/core/apps/api/tests/search/__init__.py +++ b/core/apps/api/tests/search/__init__.py @@ -1,2 +1,2 @@ -from .test_search_history import * # noqa -from .test_search_ads import * # noqa +from .test_search_history import * +from .test_search_ads import * diff --git a/core/apps/api/tests/search/test_search_ads.py b/core/apps/api/tests/search/test_search_ads.py index f585c75..c24c39d 100644 --- a/core/apps/api/tests/search/test_search_ads.py +++ b/core/apps/api/tests/search/test_search_ads.py @@ -13,7 +13,7 @@ def instance(db): @pytest.fixture def api_client(instance): client = APIClient() - ##client.force_authenticate(user=instance.user) + # client.force_authenticate(user=instance.user) return client, instance diff --git a/core/apps/api/tests/user/__init__.py b/core/apps/api/tests/user/__init__.py index 34ff1ee..9ae41b7 100644 --- a/core/apps/api/tests/user/__init__.py +++ b/core/apps/api/tests/user/__init__.py @@ -1,2 +1,2 @@ -from .test_user_like import * # noqa -from .test_user_notification import * # noqa +from .test_user_like import * +from .test_user_notification import * diff --git a/core/apps/api/translation/__init__.py b/core/apps/api/translation/__init__.py index d63c50f..63ecac4 100644 --- a/core/apps/api/translation/__init__.py +++ b/core/apps/api/translation/__init__.py @@ -1 +1 @@ -from .category import * # noqa +from .category import * diff --git a/core/apps/api/translation/category.py b/core/apps/api/translation/category.py index 9089256..2cf8857 100644 --- a/core/apps/api/translation/category.py +++ b/core/apps/api/translation/category.py @@ -1,9 +1,9 @@ from modeltranslation.translator import TranslationOptions, register -from core.apps.api.models import Category +from core.apps.api.models import CategoryModel -@register(Category) +@register(CategoryModel) class CategoryTranslation(TranslationOptions): fields = [ "name", diff --git a/core/apps/api/urls.py b/core/apps/api/urls.py index 288860a..d601d3d 100644 --- a/core/apps/api/urls.py +++ b/core/apps/api/urls.py @@ -2,25 +2,35 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter from core.apps.api.views import ( + AdViewSet, BannerViewSet, CategoryHomeApiViewSet, CategoryViewSet, - HomeAdApiView, + FeedbackViewSet, + HomeAdViewSet, NotificationViewSet, + OrderViewSet, SearchAdsViewSet, SearchHistoryViewSet, + TagViewSet, UserLikeViewSet, - AdsView, ) +from .views import ColorView, SizeView + router = DefaultRouter() -router.register("search-ads", SearchAdsViewSet, basename="search-ads") -router.register("ads", AdsView, basename="ads") -router.register("banner", BannerViewSet, basename="banner") -router.register("notification", NotificationViewSet, basename="notification") -router.register("user-like", UserLikeViewSet, basename="user-like") -router.register("category", CategoryViewSet, basename="category") -router.register("category-home", CategoryHomeApiViewSet, basename="category-home") -router.register("search-history", SearchHistoryViewSet, basename="search-history") -router.register("home-ad", HomeAdApiView, basename="home-ad") +router.register("size", SizeView, basename="size") +router.register("color", ColorView, basename="color") +router.register("ads", AdViewSet, basename="ad") +router.register("home", HomeAdViewSet, basename="home") +router.register("categories", CategoryViewSet, basename="category") +router.register("categories-home", CategoryHomeApiViewSet, basename="category-home") +router.register("search/ads", SearchAdsViewSet, basename="search-ad") +router.register("search/history", SearchHistoryViewSet, basename="search-history") +router.register("user/likes", UserLikeViewSet, basename="user-like") +router.register("user/notifications", NotificationViewSet, basename="notification") +router.register("user/orders", OrderViewSet, basename="order") +router.register("feedbacks", FeedbackViewSet, basename="feedback") +router.register("tags", TagViewSet, basename="tag") +router.register("banners", BannerViewSet, basename="banner") urlpatterns = [path("", include(router.urls))] diff --git a/core/apps/api/views/__init__.py b/core/apps/api/views/__init__.py index 29847a6..4646668 100644 --- a/core/apps/api/views/__init__.py +++ b/core/apps/api/views/__init__.py @@ -1,6 +1,10 @@ -from .category import * # noqa -from .search import * # noqa -from .ad import * # noqa -from .user import * # noqa -from .notification import * # noqa -from .banner import * # noqa +from .ad import * +from .banner import * +from .category import * +from .common import * # noqa +from .feedback import * +from .notification import * +from .order import * +from .search import * +from .tags import * +from .user import * diff --git a/core/apps/api/views/ad/__init__.py b/core/apps/api/views/ad/__init__.py index 61a687b..27facc5 100644 --- a/core/apps/api/views/ad/__init__.py +++ b/core/apps/api/views/ad/__init__.py @@ -1,2 +1,2 @@ -from .home_api import * # noqa -from .ad import * # noqa +from .home_api import * +from .ad import * diff --git a/core/apps/api/views/ad/ad.py b/core/apps/api/views/ad/ad.py index 833bc57..3ec800d 100644 --- a/core/apps/api/views/ad/ad.py +++ b/core/apps/api/views/ad/ad.py @@ -1,43 +1,70 @@ from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework.permissions import AllowAny -from drf_spectacular.utils import extend_schema +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter from django_core.mixins import BaseViewSetMixin from core.apps.api.models import AdModel from django_filters.rest_framework import DjangoFilterBackend from core.apps.api.filters import AdFilter from core.apps.api.serializers.ad.ad import ( - FullListAdSerializer, - RetrieveAdSerializer, - CreateAdSerializer, + AdListSerializer, + AdDetailSerializer, + AdCreateSerializer, ) -@extend_schema(tags=["Ads"]) -class AdsView(BaseViewSetMixin, ReadOnlyModelViewSet): - queryset = AdModel.objects.all().order_by("-created_at") - serializer_class = FullListAdSerializer +@extend_schema_view( + list=extend_schema( + summary="List all ads", + description="Get paginated list of all available ads with filtering options", + tags=["Ads"], + parameters=[ + OpenApiParameter( + name="category", + description="Filter by category ID", + required=False, + type=int + ), + OpenApiParameter( + name="min_price", + description="Minimum price filter", + required=False, + type=float + ), + OpenApiParameter( + name="max_price", + description="Maximum price filter", + required=False, + type=float + ), + ] + ), + retrieve=extend_schema( + summary="Get ad details", + description="Get detailed information about a specific ad including images, variants, and options", + tags=["Ads"] + ), +) +class AdViewSet(BaseViewSetMixin, ReadOnlyModelViewSet): + """ + ViewSet for viewing and managing ads. + + list: Get list of all available ads with filtering + retrieve: Get detailed information about a specific ad + """ + queryset = AdModel.objects.select_related( + "category", "user", "plan" + ).prefetch_related( + "images", "variants", "options", "tags" + ).filter(is_available=True).order_by("-created_at") + + serializer_class = AdListSerializer permission_classes = [AllowAny] filter_backends = [DjangoFilterBackend] filterset_class = AdFilter - action_permission_classes = {} - action_serializer_class = { - "list": FullListAdSerializer, - "retrieve": RetrieveAdSerializer, - "create": CreateAdSerializer, - } - - def list(self, request, *args, **kwargs): - queryset = self.filter_queryset(self.get_queryset()) - page = self.paginate_queryset(queryset) - - if page is not None: - data = {"ads": page} - serializer = FullListAdSerializer(data, context={"request": request}) - return self.get_paginated_response(serializer.data) - - data = {"ads": queryset} - serializer = FullListAdSerializer(data, context={"request": request}) - response = self.get_paginated_response(serializer.data) - - return response + def get_serializer_class(self): + if self.action == "retrieve": + return AdDetailSerializer + elif self.action == "create": + return AdCreateSerializer + return AdListSerializer diff --git a/core/apps/api/views/ad/home_api.py b/core/apps/api/views/ad/home_api.py index f69eef5..4f02a7d 100644 --- a/core/apps/api/views/ad/home_api.py +++ b/core/apps/api/views/ad/home_api.py @@ -1,24 +1,30 @@ from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework.permissions import AllowAny -from drf_spectacular.utils import extend_schema +from drf_spectacular.utils import extend_schema, extend_schema_view from django_core.mixins import BaseViewSetMixin from core.apps.api.models import AdModel -from core.apps.api.serializers.ad.home_api import ( - ListHomeAdSerializer, - CreateHomeAdSerializer, - RetrieveHomeAdSerializer, +from core.apps.api.serializers.ad.home_api import HomeAdListSerializer + + +@extend_schema_view( + list=extend_schema( + summary="Get home page ads", + description="Get optimized list of ads for home page (latest 20 ads)", + tags=["Home"] + ), ) - - -@extend_schema(tags=["Home Ad Api"]) -class HomeAdApiView(BaseViewSetMixin, ReadOnlyModelViewSet): - queryset = AdModel.objects.all() - serializer_class = ListHomeAdSerializer +class HomeAdViewSet(BaseViewSetMixin, ReadOnlyModelViewSet): + """ + ViewSet for home page ads - optimized for performance + + Returns latest 20 available ads without pagination + """ + queryset = AdModel.objects.select_related( + "category", "user" + ).filter( + is_available=True + ).order_by("-created_at")[:20] + + serializer_class = HomeAdListSerializer permission_classes = [AllowAny] - - action_permission_classes = {} - action_serializer_class = { - "list": ListHomeAdSerializer, - "retrieve": RetrieveHomeAdSerializer, - "create": CreateHomeAdSerializer, - } + pagination_class = None # Disable pagination for home page diff --git a/core/apps/api/views/banner/__init__.py b/core/apps/api/views/banner/__init__.py index 1b144fd..7490662 100644 --- a/core/apps/api/views/banner/__init__.py +++ b/core/apps/api/views/banner/__init__.py @@ -1 +1 @@ -from .banner import * # noqa +from .banner import * diff --git a/core/apps/api/views/banner/banner.py b/core/apps/api/views/banner/banner.py index db700ad..821f42d 100644 --- a/core/apps/api/views/banner/banner.py +++ b/core/apps/api/views/banner/banner.py @@ -1,9 +1,9 @@ from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.viewsets import ReadOnlyModelViewSet -from drf_spectacular.utils import extend_schema +from drf_spectacular.utils import extend_schema, extend_schema_view from django_core.mixins import BaseViewSetMixin -from core.apps.api.models import Banner +from core.apps.api.models import BannerModel from core.apps.api.serializers.banner import ( ListBannerSerializer, RetrieveBannerSerializer, @@ -11,15 +11,30 @@ from core.apps.api.serializers.banner import ( ) -@extend_schema(tags=['Banner']) +@extend_schema_view( + list=extend_schema( + summary="List all active banners", + description="Get list of all active banners ordered by display order", + tags=["Banners"] + ), + retrieve=extend_schema( + summary="Get banner details", + description="Get detailed information about a specific banner", + tags=["Banners"] + ), +) class BannerViewSet(BaseViewSetMixin, ReadOnlyModelViewSet): - queryset = Banner.objects.all() + """ + ViewSet for managing banners. + + Returns active banners for display on website. + """ + queryset = BannerModel.objects.filter(is_active=True).order_by("order", "-created_at") serializer_class = ListBannerSerializer permission_classes = [AllowAny] - action_permission_classes = {} - action_serializers = { - 'list': ListBannerSerializer, - 'retrieve': RetrieveBannerSerializer, - 'create': CreateBannerSerializer, + action_serializer_class = { + "list": ListBannerSerializer, + "retrieve": RetrieveBannerSerializer, + "create": CreateBannerSerializer, } diff --git a/core/apps/api/views/category/__init__.py b/core/apps/api/views/category/__init__.py index d63c50f..63ecac4 100644 --- a/core/apps/api/views/category/__init__.py +++ b/core/apps/api/views/category/__init__.py @@ -1 +1 @@ -from .category import * # noqa +from .category import * diff --git a/core/apps/api/views/category/category.py b/core/apps/api/views/category/category.py index f3596cd..676b8d2 100644 --- a/core/apps/api/views/category/category.py +++ b/core/apps/api/views/category/category.py @@ -1,8 +1,8 @@ from rest_framework.permissions import AllowAny from rest_framework.viewsets import ReadOnlyModelViewSet from django_core.mixins.base import BaseViewSetMixin -from drf_spectacular.utils import extend_schema -from core.apps.api.models import Category +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter +from core.apps.api.models import CategoryModel from django_filters.rest_framework import DjangoFilterBackend from core.apps.api.filters.category import CategoryFilter from core.apps.api.serializers.category import ( @@ -13,16 +13,45 @@ from core.apps.api.serializers.category import ( ) -@extend_schema(tags=["Category"]) +@extend_schema_view( + list=extend_schema( + summary="List all categories", + description="Get hierarchical list of all root categories with their children", + tags=["Categories"], + parameters=[ + OpenApiParameter( + name="show_home", + description="Filter categories shown on home page", + required=False, + type=bool + ), + OpenApiParameter( + name="category_type", + description="Filter by category type (product/service)", + required=False, + type=str + ), + ] + ), + retrieve=extend_schema( + summary="Get category details", + description="Get detailed information about a specific category", + tags=["Categories"] + ), +) class CategoryViewSet(BaseViewSetMixin, ReadOnlyModelViewSet): - queryset = Category.objects.filter(level=0) + """ + ViewSet for managing categories. + + Returns hierarchical category structure. + """ + queryset = CategoryModel.objects.filter(level=0) permission_classes = [AllowAny] serializer_class = ListCategorySerializer pagination_class = None filter_backends = [DjangoFilterBackend] filterset_class = CategoryFilter - action_permission_classes = {} action_serializer_class = { "list": ListCategorySerializer, "retrieve": RetrieveCategorySerializer, @@ -30,16 +59,26 @@ class CategoryViewSet(BaseViewSetMixin, ReadOnlyModelViewSet): } -@extend_schema(tags=["Category"]) +@extend_schema_view( + list=extend_schema( + summary="Get home page categories", + description="Get flat list of all categories for home page display", + tags=["Categories"] + ), +) class CategoryHomeApiViewSet(BaseViewSetMixin, ReadOnlyModelViewSet): - queryset = Category.objects.all() + """ + ViewSet for home page categories. + + Returns flat list without hierarchy. + """ + queryset = CategoryModel.objects.all() permission_classes = [AllowAny] serializer_class = ListCategoryNoChildSerializer pagination_class = None filter_backends = [DjangoFilterBackend] filterset_class = CategoryFilter - action_permission_classes = {} action_serializer_class = { "list": ListCategoryNoChildSerializer, "retrieve": RetrieveCategorySerializer, diff --git a/core/apps/api/views/common.py b/core/apps/api/views/common.py new file mode 100644 index 0000000..23b1f64 --- /dev/null +++ b/core/apps/api/views/common.py @@ -0,0 +1,42 @@ +from django_core.mixins import BaseViewSetMixin +from drf_spectacular.utils import extend_schema +from rest_framework.permissions import AllowAny +from rest_framework.viewsets import ReadOnlyModelViewSet + +from core.apps.api.models import ColorModel, SizeModel +from core.apps.api.serializers.common import ( + CreateColorSerializer, + CreateSizeSerializer, + ListColorSerializer, + ListSizeSerializer, + RetrieveColorSerializer, + RetrieveSizeSerializer, +) + + +@extend_schema(tags=["color"]) +class ColorView(BaseViewSetMixin, ReadOnlyModelViewSet): + queryset = ColorModel.objects.all() + serializer_class = ListColorSerializer + permission_classes = [AllowAny] + + action_permission_classes = {} + action_serializer_class = { + "list": ListColorSerializer, + "retrieve": RetrieveColorSerializer, + "create": CreateColorSerializer, + } + + +@extend_schema(tags=["size"]) +class SizeView(BaseViewSetMixin, ReadOnlyModelViewSet): + queryset = SizeModel.objects.all() + serializer_class = ListSizeSerializer + permission_classes = [AllowAny] + + action_permission_classes = {} + action_serializer_class = { + "list": ListSizeSerializer, + "retrieve": RetrieveSizeSerializer, + "create": CreateSizeSerializer, + } diff --git a/core/apps/api/views/feedback/__init__.py b/core/apps/api/views/feedback/__init__.py new file mode 100644 index 0000000..c2ed394 --- /dev/null +++ b/core/apps/api/views/feedback/__init__.py @@ -0,0 +1 @@ +from .feedback import * diff --git a/core/apps/api/views/feedback/feedback.py b/core/apps/api/views/feedback/feedback.py new file mode 100644 index 0000000..719907c --- /dev/null +++ b/core/apps/api/views/feedback/feedback.py @@ -0,0 +1,95 @@ +from rest_framework.viewsets import ModelViewSet +from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAuthenticated +from rest_framework.decorators import action +from rest_framework.response import Response +from drf_spectacular.utils import extend_schema, extend_schema_view +from django_core.mixins import BaseViewSetMixin +from core.apps.api.models import FeedbackModel +from core.apps.api.serializers.feedback import ( + FeedbackListSerializer, + FeedbackDetailSerializer, + FeedbackCreateSerializer, + FeedbackUpdateSerializer, +) + + +@extend_schema_view( + list=extend_schema( + summary="List all feedbacks", description="Get list of all product feedbacks/reviews", tags=["Feedbacks"] + ), + retrieve=extend_schema( + summary="Get feedback details", + description="Get detailed information about a specific feedback", + tags=["Feedbacks"], + ), + create=extend_schema( + summary="Create feedback", description="Create a new feedback/review for a product", tags=["Feedbacks"] + ), + update=extend_schema(summary="Update feedback", description="Update your own feedback", tags=["Feedbacks"]), + partial_update=extend_schema( + summary="Partially update feedback", description="Partially update your own feedback", tags=["Feedbacks"] + ), + destroy=extend_schema(summary="Delete feedback", description="Delete your own feedback", tags=["Feedbacks"]), +) +class FeedbackViewSet(BaseViewSetMixin, ModelViewSet): + """ + ViewSet for managing product feedbacks/reviews. + + Users can create, read, update and delete their own feedbacks. + """ + + queryset = FeedbackModel.objects.select_related("user", "ad").prefetch_related("images").order_by("-created_at") + + permission_classes = [IsAuthenticatedOrReadOnly] + + def get_queryset(self): + queryset = super().get_queryset() + ad_id = self.request.query_params.get("ad") + if ad_id: + queryset = queryset.filter(ad_id=ad_id) + user_id = self.request.query_params.get("user") + if user_id: + queryset = queryset.filter(user_id=user_id) + star = self.request.query_params.get("star") + if star: + queryset = queryset.filter(star=star) + return queryset + + def get_serializer_class(self): + if self.action == "create": + return FeedbackCreateSerializer + elif self.action in ["update", "partial_update"]: + return FeedbackUpdateSerializer + elif self.action == "retrieve": + return FeedbackDetailSerializer + return FeedbackListSerializer + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + def perform_update(self, serializer): + if serializer.instance.user != self.request.user: + raise PermissionError("You can only update your own feedback") + serializer.save() + + def perform_destroy(self, instance): + # Only allow users to delete their own feedback + if instance.user != self.request.user: + raise PermissionError("You can only delete your own feedback") + instance.delete() + + @extend_schema( + summary="Get my feedbacks", description="Get all feedbacks created by the current user", tags=["Feedbacks"] + ) + @action(detail=False, methods=["get"], permission_classes=[IsAuthenticated]) + def my_feedbacks(self, request): + """Get current user's feedbacks""" + queryset = self.get_queryset().filter(user=request.user) + page = self.paginate_queryset(queryset) + + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) diff --git a/core/apps/api/views/notification/__init__.py b/core/apps/api/views/notification/__init__.py index 9d441eb..1c164b2 100644 --- a/core/apps/api/views/notification/__init__.py +++ b/core/apps/api/views/notification/__init__.py @@ -1 +1 @@ -from .notification import * # noqa +from .notification import * diff --git a/core/apps/api/views/notification/notification.py b/core/apps/api/views/notification/notification.py index 5b103db..a1205a1 100644 --- a/core/apps/api/views/notification/notification.py +++ b/core/apps/api/views/notification/notification.py @@ -24,7 +24,6 @@ class NotificationViewSet(BaseViewSetMixin, ReadOnlyModelViewSet): serializer_class = ListUserNotificationSerializer filter_backends = [DjangoFilterBackend] - action_permission_classes = {} action_serializer_class = { "list": ListUserNotificationSerializer, "retrieve": RetrieveUserNotificationSerializer, diff --git a/core/apps/api/views/order/__init__.py b/core/apps/api/views/order/__init__.py new file mode 100644 index 0000000..4ca2c09 --- /dev/null +++ b/core/apps/api/views/order/__init__.py @@ -0,0 +1 @@ +from .order import * diff --git a/core/apps/api/views/order/order.py b/core/apps/api/views/order/order.py new file mode 100644 index 0000000..ad0370b --- /dev/null +++ b/core/apps/api/views/order/order.py @@ -0,0 +1,88 @@ +# type: ignore +from rest_framework.viewsets import ModelViewSet +from rest_framework.permissions import IsAuthenticated +from rest_framework.decorators import action +from rest_framework.response import Response +from drf_spectacular.utils import extend_schema, extend_schema_view +from django_core.mixins import BaseViewSetMixin +from core.apps.api.models import OrderModel +from core.apps.api.serializers.order import ( + OrderListSerializer, + OrderDetailSerializer, + OrderCreateSerializer, + OrderUpdateSerializer, +) + + +@extend_schema_view( + list=extend_schema( + summary="List all orders", description="Get list of all orders for the current user", tags=["Orders"] + ), + retrieve=extend_schema( + summary="Get order details", description="Get detailed information about a specific order", tags=["Orders"] + ), + create=extend_schema(summary="Create order", description="Create a new order with items", tags=["Orders"]), + update=extend_schema(summary="Update order", description="Update order status or address", tags=["Orders"]), + partial_update=extend_schema( + summary="Partially update order", description="Partially update order information", tags=["Orders"] + ), + destroy=extend_schema(summary="Cancel order", description="Cancel/delete an order", tags=["Orders"]), +) +class OrderViewSet(BaseViewSetMixin, ModelViewSet): + """ + ViewSet for managing orders. + + Users can only see and manage their own orders. + """ + + permission_classes = [IsAuthenticated] + + def get_queryset(self): + queryset = ( + OrderModel.objects.select_related("user", "address") + .prefetch_related("items__ad") + .filter(user=self.request.user) + .order_by("-created_at") + ) + + # Filter by status + status_filter = self.request.query_params.get("status") + if status_filter: + queryset = queryset.filter(status=status_filter) + + return queryset + + def get_serializer_class(self): + if self.action == "create": + return OrderCreateSerializer + elif self.action in ["update", "partial_update"]: + return OrderUpdateSerializer + elif self.action == "retrieve": + return OrderDetailSerializer + return OrderListSerializer + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + def perform_destroy(self, instance): + # Only allow cancellation of pending orders + if instance.status not in ["pending", "processing"]: + raise PermissionError("Can only cancel pending or processing orders") + instance.delete() + + @extend_schema(summary="Get order statistics", description="Get statistics about user's orders", tags=["Orders"]) + @action(detail=False, methods=["get"]) + def statistics(self, request): + """Get order statistics for current user""" + queryset = self.get_queryset() + + stats = { + "total_orders": queryset.count(), + "pending": queryset.filter(status="pending").count(), + "processing": queryset.filter(status="processing").count(), + "completed": queryset.filter(status="completed").count(), + "cancelled": queryset.filter(status="cancelled").count(), + "total_spent": sum(order.total_amount for order in queryset), + } + + return Response(stats) diff --git a/core/apps/api/views/search/__init__.py b/core/apps/api/views/search/__init__.py index 6287097..1d13162 100644 --- a/core/apps/api/views/search/__init__.py +++ b/core/apps/api/views/search/__init__.py @@ -1,2 +1,2 @@ -from .search import * # noqa -from .search_ads import * # noqa +from .history import * +from .ad import * diff --git a/core/apps/api/views/search/search_ads.py b/core/apps/api/views/search/ad.py similarity index 97% rename from core/apps/api/views/search/search_ads.py rename to core/apps/api/views/search/ad.py index 58264e2..70fb163 100644 --- a/core/apps/api/views/search/search_ads.py +++ b/core/apps/api/views/search/ad.py @@ -21,7 +21,6 @@ class SearchAdsViewSet(BaseViewSetMixin, mixins.ListModelMixin, GenericViewSet): filter_backends = [DjangoFilterBackend, SearchFilter] search_fields = ["name"] - action_permission_classes = {} action_serializer_class = { 'list': ListSearchAdsSerializer, } diff --git a/core/apps/api/views/search/search.py b/core/apps/api/views/search/history.py similarity index 96% rename from core/apps/api/views/search/search.py rename to core/apps/api/views/search/history.py index 8ac3d2f..fd7421e 100644 --- a/core/apps/api/views/search/search.py +++ b/core/apps/api/views/search/history.py @@ -18,7 +18,7 @@ class SearchHistoryViewSet(BaseViewSetMixin, mixins.ListModelMixin, mixins.Creat serializer_class = ListSearchHistorySerializer permission_classes = [IsAuthenticated] http_method_names = ['get', 'post', 'delete'] - action_permission_classes = {} + action_serializer_class = { 'list': ListSearchHistorySerializer, 'create': CreateSearchHistorySerializer, diff --git a/core/apps/api/views/tags/__init__.py b/core/apps/api/views/tags/__init__.py new file mode 100644 index 0000000..11b89a3 --- /dev/null +++ b/core/apps/api/views/tags/__init__.py @@ -0,0 +1 @@ +from .tags import * diff --git a/core/apps/api/views/tags/tags.py b/core/apps/api/views/tags/tags.py new file mode 100644 index 0000000..f70a69f --- /dev/null +++ b/core/apps/api/views/tags/tags.py @@ -0,0 +1,30 @@ +from rest_framework.viewsets import ModelViewSet +from rest_framework.permissions import IsAuthenticatedOrReadOnly +from rest_framework.filters import SearchFilter +from drf_spectacular.utils import extend_schema, extend_schema_view +from django_core.mixins import BaseViewSetMixin +from core.apps.api.models import TagsModel, ColorModel +from core.apps.api.serializers.tags import TagSerializer, ColorSerializer + + +@extend_schema_view( + list=extend_schema(summary="List all tags", description="Get list of all available tags", tags=["Tags"]), + retrieve=extend_schema( + summary="Get tag details", description="Get detailed information about a specific tag", tags=["Tags"] + ), + create=extend_schema(summary="Create tag", description="Create a new tag (Admin only)", tags=["Tags"]), + update=extend_schema(summary="Update tag", description="Update tag information (Admin only)", tags=["Tags"]), + destroy=extend_schema(summary="Delete tag", description="Delete a tag (Admin only)", tags=["Tags"]), +) +class TagViewSet(BaseViewSetMixin, ModelViewSet): + """ + ViewSet for managing tags. + + Everyone can read, only authenticated users can create/modify. + """ + + queryset = TagsModel.objects.all().order_by("name") + serializer_class = TagSerializer + permission_classes = [IsAuthenticatedOrReadOnly] + filter_backends = [SearchFilter] + search_fields = ["name", "slug"] diff --git a/core/apps/api/views/user/__init__.py b/core/apps/api/views/user/__init__.py index 54d9396..f25faa8 100644 --- a/core/apps/api/views/user/__init__.py +++ b/core/apps/api/views/user/__init__.py @@ -1 +1 @@ -from .ad_like import * # noqa +from .like import * diff --git a/core/apps/api/views/user/ad_like.py b/core/apps/api/views/user/ad_like.py deleted file mode 100644 index 0f53917..0000000 --- a/core/apps/api/views/user/ad_like.py +++ /dev/null @@ -1,28 +0,0 @@ -from rest_framework import mixins -from rest_framework.viewsets import GenericViewSet -from django_core.mixins.base import BaseViewSetMixin -from drf_spectacular.utils import extend_schema -from rest_framework.permissions import IsAuthenticated -from core.apps.accounts.models import UserLike -from core.apps.api.serializers.user.ad_like import ( - ListUserLikeSerializer, - CreateUserLikeSerializer, - -) - - -@extend_schema(tags=['User Like']) -class UserLikeViewSet(BaseViewSetMixin, mixins.ListModelMixin, mixins.CreateModelMixin, - mixins.DestroyModelMixin, GenericViewSet): - serializer_class = ListUserLikeSerializer - permission_classes = [IsAuthenticated] - http_method_names = ['get', 'post', 'delete'] - action_permission_classes = {} - action_serializer_class = { - 'list': ListUserLikeSerializer, - 'create': CreateUserLikeSerializer, - } - - def get_queryset(self): - queryset = UserLike.objects.filter(user=self.request.user).order_by('-id') - return queryset diff --git a/core/apps/api/views/user/like.py b/core/apps/api/views/user/like.py new file mode 100644 index 0000000..35ca731 --- /dev/null +++ b/core/apps/api/views/user/like.py @@ -0,0 +1,40 @@ +from rest_framework import mixins +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet +from django_core.mixins.base import BaseViewSetMixin +from drf_spectacular.utils import extend_schema +from rest_framework.permissions import IsAuthenticated +from core.apps.accounts.models import UserLike +from core.apps.api.serializers.user.like import ( + ListUserLikeSerializer, + CreateUserLikeSerializer, +) + + +@extend_schema(tags=["User Like"]) +class UserLikeViewSet(BaseViewSetMixin, mixins.ListModelMixin, GenericViewSet): + serializer_class = ListUserLikeSerializer + permission_classes = [IsAuthenticated] + http_method_names = ["get", "post", "delete"] + + action_serializer_class = { + "list": ListUserLikeSerializer, + "create": CreateUserLikeSerializer, + } + + def create(self, request): + ser = self.get_serializer(data=request.data) + ser.is_valid(raise_exception=True) + instance = UserLike.objects.filter(user=request.user, ad=ser.validated_data.get("ad")).first() + if instance is not None: + instance.delete() + return Response(data={"detail": "deleted"}) + self.perform_create(ser) + return Response(data={"detail": "created"}) + + def get_queryset(self): + queryset = UserLike.objects.filter(user=self.request.user).order_by("-id") + return queryset + + def perform_create(self, serializer): + serializer.save(user=self.request.user)