From 5a0ff9ddcf0618ebeae7109792964f216938ca9e Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Tue, 31 Oct 2023 19:52:27 +0500 Subject: [PATCH 1/9] promo_check --- api_spot/api/serializers/__init__.py | 2 ++ api_spot/api/serializers/promocode_check.py | 10 ++++++++++ api_spot/api/services/promocode_check.py | 13 +++++++++++++ api_spot/api/urls.py | 5 +++-- api_spot/api/views/__init__.py | 2 ++ api_spot/api/views/promocode_check.py | 20 ++++++++++++++++++++ 6 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 api_spot/api/serializers/promocode_check.py create mode 100644 api_spot/api/services/promocode_check.py create mode 100644 api_spot/api/views/promocode_check.py diff --git a/api_spot/api/serializers/__init__.py b/api_spot/api/serializers/__init__.py index c00c2f8e..c45659a6 100644 --- a/api_spot/api/serializers/__init__.py +++ b/api_spot/api/serializers/__init__.py @@ -6,6 +6,7 @@ LocationGetSerializer, LocationGetShortSerializer, LocationMapSerializer, ) from .plan_photo import PlanPhotoGetSerializer +from .promocode_check import PromocodeCheckSerializer from .question import QuestionSerializer from .rule import RuleSerializer from .spot import SpotDetailSerializer, SpotQuerySerializer, SpotSerializer @@ -19,6 +20,7 @@ LocationGetShortSerializer LocationMapSerializer PlanPhotoGetSerializer +PromocodeCheckSerializer QuestionSerializer RuleSerializer SpotDetailSerializer diff --git a/api_spot/api/serializers/promocode_check.py b/api_spot/api/serializers/promocode_check.py new file mode 100644 index 00000000..8f77bc99 --- /dev/null +++ b/api_spot/api/serializers/promocode_check.py @@ -0,0 +1,10 @@ +from rest_framework import serializers + +from api.services.promocode_check import promocode_available_check + + +class PromocodeCheckSerializer(serializers.Serializer): + promocode = serializers.CharField(max_length=64) + + def validate_promocode(self, value, *args, **kwargs): + promocode_available_check(value) diff --git a/api_spot/api/services/promocode_check.py b/api_spot/api/services/promocode_check.py new file mode 100644 index 00000000..cf5ce3ba --- /dev/null +++ b/api_spot/api/services/promocode_check.py @@ -0,0 +1,13 @@ +from django.utils import timezone +from rest_framework.exceptions import ValidationError + +from promo.models import Promocode + + +def promocode_available_check(value): + promocode = Promocode.objects.filter(name=value).first() + if promocode: + if promocode.balance < 1: + raise ValidationError('Количество использований исчерпано.') + if promocode.expiry_date < timezone.now().date(): + raise ValidationError('Срок годности промокода истек.') diff --git a/api_spot/api/urls.py b/api_spot/api/urls.py index 89f259c1..16ec5b9b 100644 --- a/api_spot/api/urls.py +++ b/api_spot/api/urls.py @@ -6,7 +6,7 @@ FavoriteViewSet, LocationMapListAPIView, LocationShortListAPIView, LocationViewSet, OrderGetViewSet, OrderViewSet, PayView, PlanPhotoAPIView, QuestionViewSet, ReviewCreateViewSet, ReviewGetViewSet, RuleViewSet, - SpotViewSet, SubscireAPIView, UserViewSet, + SpotViewSet, SubscireAPIView, UserViewSet, PromocodeCheckAPIView, ) @@ -77,7 +77,8 @@ ), path('short_locations/', LocationShortListAPIView.as_view()), path('map_locations/', LocationMapListAPIView.as_view()), - path('subscribe/', SubscireAPIView.as_view(),), + path('subscribe/', SubscireAPIView.as_view()), + path('promocode_check/', PromocodeCheckAPIView.as_view()), re_path( r'locations/(?P\d+)/plan_photo/', PlanPhotoAPIView.as_view() diff --git a/api_spot/api/views/__init__.py b/api_spot/api/views/__init__.py index 81053f15..ffaaee6a 100644 --- a/api_spot/api/views/__init__.py +++ b/api_spot/api/views/__init__.py @@ -9,6 +9,7 @@ from .order import OrderGetViewSet, OrderViewSet from .pay import PayView from .plan_photo import PlanPhotoAPIView +from .promocode_check import PromocodeCheckAPIView from .question import QuestionViewSet from .review import ReviewCreateViewSet, ReviewGetViewSet from .rule import RuleViewSet @@ -29,6 +30,7 @@ OrderViewSet PayView PlanPhotoAPIView +PromocodeCheckAPIView QuestionViewSet ReviewCreateViewSet ReviewGetViewSet diff --git a/api_spot/api/views/promocode_check.py b/api_spot/api/views/promocode_check.py new file mode 100644 index 00000000..4163e745 --- /dev/null +++ b/api_spot/api/views/promocode_check.py @@ -0,0 +1,20 @@ +from drf_spectacular.utils import extend_schema +from rest_framework import status +from rest_framework.generics import CreateAPIView +from rest_framework.response import Response + +from api.serializers.promocode_check import PromocodeCheckSerializer + + +@extend_schema( + tags=('pay',), +) +class PromocodeCheckAPIView(CreateAPIView): + serializer_class = PromocodeCheckSerializer + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + return Response( + {'message': 'Промокод доступен'}, status=status.HTTP_200_OK + ) From 646267c84cbeb83704e382fe786f500314aa57fd Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Tue, 31 Oct 2023 19:54:40 +0500 Subject: [PATCH 2/9] fix --- api_spot/api/services/promocode_check.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api_spot/api/services/promocode_check.py b/api_spot/api/services/promocode_check.py index cf5ce3ba..4b52790b 100644 --- a/api_spot/api/services/promocode_check.py +++ b/api_spot/api/services/promocode_check.py @@ -11,3 +11,5 @@ def promocode_available_check(value): raise ValidationError('Количество использований исчерпано.') if promocode.expiry_date < timezone.now().date(): raise ValidationError('Срок годности промокода истек.') + else: + ValidationError('Промокода не существует') From 0242514497ee4ccc687f761b88605ad386eba4e2 Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Tue, 31 Oct 2023 19:56:41 +0500 Subject: [PATCH 3/9] fix1 --- api_spot/api/services/promocode_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api_spot/api/services/promocode_check.py b/api_spot/api/services/promocode_check.py index 4b52790b..fe9c5978 100644 --- a/api_spot/api/services/promocode_check.py +++ b/api_spot/api/services/promocode_check.py @@ -12,4 +12,4 @@ def promocode_available_check(value): if promocode.expiry_date < timezone.now().date(): raise ValidationError('Срок годности промокода истек.') else: - ValidationError('Промокода не существует') + raise ValidationError('Промокода не существует') From 66aae8ad9636c27b14482e278e1949e0563e7875 Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Tue, 31 Oct 2023 19:57:45 +0500 Subject: [PATCH 4/9] isort --- api_spot/api/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api_spot/api/urls.py b/api_spot/api/urls.py index 16ec5b9b..19742164 100644 --- a/api_spot/api/urls.py +++ b/api_spot/api/urls.py @@ -5,8 +5,8 @@ AddSpotsAPIView, AvatarViewSet, EquipmentViewSet, EventViewSet, FavoriteViewSet, LocationMapListAPIView, LocationShortListAPIView, LocationViewSet, OrderGetViewSet, OrderViewSet, PayView, PlanPhotoAPIView, - QuestionViewSet, ReviewCreateViewSet, ReviewGetViewSet, RuleViewSet, - SpotViewSet, SubscireAPIView, UserViewSet, PromocodeCheckAPIView, + PromocodeCheckAPIView, QuestionViewSet, ReviewCreateViewSet, + ReviewGetViewSet, RuleViewSet, SpotViewSet, SubscireAPIView, UserViewSet, ) From daaa1eaf67a22acf9578a7c0c24268340c0c49d1 Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Tue, 31 Oct 2023 20:28:36 +0500 Subject: [PATCH 5/9] s3static --- api_spot/api_spot/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api_spot/api_spot/settings.py b/api_spot/api_spot/settings.py index 69212396..ac49c2ea 100644 --- a/api_spot/api_spot/settings.py +++ b/api_spot/api_spot/settings.py @@ -212,7 +212,7 @@ } }, 'staticfiles': { - 'BACKEND': 'storages.backends.s3.S3Storage', + 'BACKEND': 'storages.backends.s3.S3StaticStorage', 'OPTIONS': { 'bucket_name': 'static', 'access_key': AWS_ACCESS_KEY_ID, From ac76c50a55abbc59973aecda065aa91f8514712c Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Tue, 31 Oct 2023 20:32:14 +0500 Subject: [PATCH 6/9] del expire --- api_spot/api_spot/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api_spot/api_spot/settings.py b/api_spot/api_spot/settings.py index ac49c2ea..9ba039af 100644 --- a/api_spot/api_spot/settings.py +++ b/api_spot/api_spot/settings.py @@ -220,7 +220,6 @@ 'region_name': AWS_S3_REGION_NAME, 'use_ssl': AWS_S3_USE_SSL, 'endpoint_url': AWS_S3_ENDPOINT_URL, - 'querystring_expire': 5 * 24 * 60 * 60, } }, } From 5105d54358cd3fbe3905440f4072eaeb3a6749c6 Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Wed, 1 Nov 2023 04:10:48 +0500 Subject: [PATCH 7/9] promov2 --- api_spot/api/serializers/promocode_check.py | 16 ++++++- api_spot/api/services/promocode_check.py | 14 +++++- api_spot/api/urls.py | 6 ++- api_spot/api/views/promocode_check.py | 10 +++- api_spot/promo/admin.py | 14 +++++- ...ne_off_promocode_only_category_and_more.py | 47 +++++++++++++++++++ api_spot/promo/models.py | 44 +++++++++++++++++ .../migrations/0015_alter_price_discount.py | 19 ++++++++ 8 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 api_spot/promo/migrations/0005_promocode_one_off_promocode_only_category_and_more.py create mode 100644 api_spot/spots/migrations/0015_alter_price_discount.py diff --git a/api_spot/api/serializers/promocode_check.py b/api_spot/api/serializers/promocode_check.py index 8f77bc99..598f7bd6 100644 --- a/api_spot/api/serializers/promocode_check.py +++ b/api_spot/api/serializers/promocode_check.py @@ -1,10 +1,22 @@ from rest_framework import serializers from api.services.promocode_check import promocode_available_check +from api.fields import GetSpot class PromocodeCheckSerializer(serializers.Serializer): promocode = serializers.CharField(max_length=64) + order = serializers.HiddenField( + default=GetSpot() + ) + user = serializers.HiddenField( + default=serializers.CurrentUserDefault() + ) - def validate_promocode(self, value, *args, **kwargs): - promocode_available_check(value) + def validate(self, data, *args, **kwargs): + promocode_name = data.get('promocode') + spot = data.get('order') + user = data.get('user') + promocode = promocode_available_check(promocode_name, spot, user) + data['promocode'] = promocode + return data diff --git a/api_spot/api/services/promocode_check.py b/api_spot/api/services/promocode_check.py index fe9c5978..1733a51f 100644 --- a/api_spot/api/services/promocode_check.py +++ b/api_spot/api/services/promocode_check.py @@ -4,12 +4,22 @@ from promo.models import Promocode -def promocode_available_check(value): - promocode = Promocode.objects.filter(name=value).first() +def promocode_available_check(promocode_name, spot, user): + """ + Проверка доступности использования промокода. + """ + promocode = Promocode.objects.filter(name=promocode_name).first() if promocode: if promocode.balance < 1: raise ValidationError('Количество использований исчерпано.') if promocode.expiry_date < timezone.now().date(): raise ValidationError('Срок годности промокода истек.') + if (promocode.only_category + and promocode.only_category != spot.category): + raise ValidationError('Промокод не применяется для этой категории') + used_by_user = promocode.promocode_user.filter(user=user).exists() + if promocode.one_off and used_by_user: + raise ValidationError('Вы уже использовали данный промокод') + return promocode else: raise ValidationError('Промокода не существует') diff --git a/api_spot/api/urls.py b/api_spot/api/urls.py index 19742164..8e6c53d5 100644 --- a/api_spot/api/urls.py +++ b/api_spot/api/urls.py @@ -75,10 +75,14 @@ r'/order/(?P\d+)/pay/', PayView.as_view(), name='pay' ), + re_path( + r'locations/(?P\d+)/spots/(?P\d+)' + r'/order/(?P\d+)/check_promocode/', + PromocodeCheckAPIView.as_view(), name='check_promocode' + ), path('short_locations/', LocationShortListAPIView.as_view()), path('map_locations/', LocationMapListAPIView.as_view()), path('subscribe/', SubscireAPIView.as_view()), - path('promocode_check/', PromocodeCheckAPIView.as_view()), re_path( r'locations/(?P\d+)/plan_photo/', PlanPhotoAPIView.as_view() diff --git a/api_spot/api/views/promocode_check.py b/api_spot/api/views/promocode_check.py index 4163e745..1b055046 100644 --- a/api_spot/api/views/promocode_check.py +++ b/api_spot/api/views/promocode_check.py @@ -1,6 +1,7 @@ from drf_spectacular.utils import extend_schema from rest_framework import status from rest_framework.generics import CreateAPIView +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from api.serializers.promocode_check import PromocodeCheckSerializer @@ -10,11 +11,18 @@ tags=('pay',), ) class PromocodeCheckAPIView(CreateAPIView): + """ + Представление доступности проверки промокода. + """ serializer_class = PromocodeCheckSerializer + permission_classes = (IsAuthenticated,) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) + promocode = serializer.validated_data.get('promocode') + discount = promocode.percent_discount return Response( - {'message': 'Промокод доступен'}, status=status.HTTP_200_OK + {'message': f'Промокод доступен на скидку {discount} %'}, + status=status.HTTP_200_OK, ) diff --git a/api_spot/promo/admin.py b/api_spot/promo/admin.py index b2bb9a97..9546215b 100644 --- a/api_spot/promo/admin.py +++ b/api_spot/promo/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from promo.models import EmailNews, Promocode +from promo.models import EmailNews, Promocode, PromocodeUser @admin.register(Promocode) @@ -11,9 +11,21 @@ class PromcodeAdmin(admin.ModelAdmin): 'percent_discount', 'expiry_date', 'balance', + 'only_category', + 'one_off', ) +@admin.register(PromocodeUser) +class PromocodeUserAdmin(admin.ModelAdmin): + list_display = ( + 'id', + 'user', + 'promocode', + ) + list_filter = ('promocode',) + + @admin.register(EmailNews) class EmailNewsAdmin(admin.ModelAdmin): list_display = ( diff --git a/api_spot/promo/migrations/0005_promocode_one_off_promocode_only_category_and_more.py b/api_spot/promo/migrations/0005_promocode_one_off_promocode_only_category_and_more.py new file mode 100644 index 00000000..d43e6cf4 --- /dev/null +++ b/api_spot/promo/migrations/0005_promocode_one_off_promocode_only_category_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.5 on 2023-10-31 22:13 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('promo', '0004_alter_promocode_expiry_date'), + ] + + operations = [ + migrations.AddField( + model_name='promocode', + name='one_off', + field=models.BooleanField(default=True, verbose_name='Однократное использование пользователем'), + ), + migrations.AddField( + model_name='promocode', + name='only_category', + field=models.CharField(blank=True, choices=[('Рабочее место', 'Рабочее место'), ('Переговорная', 'Переговорная')], null=True, verbose_name='Только для категории'), + ), + migrations.CreateModel( + name='PromocodeUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('promocode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='promocode_user', to='promo.promocode')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='promocode_user', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Пользователь-промокод', + 'verbose_name_plural': 'Пользователи-промокоды', + }, + ), + migrations.AddField( + model_name='promocode', + name='used_user', + field=models.ManyToManyField(related_name='promocode', through='promo.PromocodeUser', to=settings.AUTH_USER_MODEL, verbose_name='Оборудование'), + ), + migrations.AddConstraint( + model_name='promocodeuser', + constraint=models.UniqueConstraint(fields=('user', 'promocode'), name='unique_user_promocode'), + ), + ] diff --git a/api_spot/promo/models.py b/api_spot/promo/models.py index c5c74da1..b2d6011e 100644 --- a/api_spot/promo/models.py +++ b/api_spot/promo/models.py @@ -1,3 +1,4 @@ +from django.contrib.auth import get_user_model from django.db import models, transaction from django_celery_beat.models import PeriodicTask @@ -6,6 +7,10 @@ MaxDiscountValidator, validate_date_less_present, validate_datetime_less_present, ) +from spots.constants import CATEGORY_CHOICES + + +User = get_user_model() class Promocode(models.Model): @@ -25,6 +30,22 @@ class Promocode(models.Model): balance = models.PositiveIntegerField( 'Количество использований', ) + only_category = models.CharField( + 'Только для категории', + choices=CATEGORY_CHOICES, + blank=True, + null=True, + ) + one_off = models.BooleanField( + 'Однократное использование пользователем', + default=True, + ) + used_user = models.ManyToManyField( + User, + related_name='promocode', + verbose_name='Оборудование', + through='PromocodeUser', + ) def __str__(self): return self.name @@ -35,6 +56,29 @@ class Meta: ordering = ('expiry_date',) +class PromocodeUser(models.Model): + user = models.ForeignKey( + User, + related_name='promocode_user', + on_delete=models.CASCADE, + ) + promocode = models.ForeignKey( + Promocode, + related_name='promocode_user', + on_delete=models.CASCADE, + ) + + class Meta: + verbose_name = 'Пользователь-промокод' + verbose_name_plural = 'Пользователи-промокоды' + constraints = ( + models.UniqueConstraint( + fields=('user', 'promocode'), + name='unique_user_promocode', + ), + ) + + class EmailNews(models.Model): subject_message = models.CharField( 'Тема письма', diff --git a/api_spot/spots/migrations/0015_alter_price_discount.py b/api_spot/spots/migrations/0015_alter_price_discount.py new file mode 100644 index 00000000..afd409e7 --- /dev/null +++ b/api_spot/spots/migrations/0015_alter_price_discount.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.5 on 2023-10-31 22:13 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spots', '0014_alter_order_options'), + ] + + operations = [ + migrations.AlterField( + model_name='price', + name='discount', + field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MaxValueValidator(limit_value=70, message='Скидка не может превышать 70%%')], verbose_name='Скидка'), + ), + ] From 4060bc69eec46c45b4033d726c59781f413ffbac Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Wed, 1 Nov 2023 04:18:30 +0500 Subject: [PATCH 8/9] fix docstring --- api_spot/api/views/promocode_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api_spot/api/views/promocode_check.py b/api_spot/api/views/promocode_check.py index 1b055046..bfd67943 100644 --- a/api_spot/api/views/promocode_check.py +++ b/api_spot/api/views/promocode_check.py @@ -12,7 +12,7 @@ ) class PromocodeCheckAPIView(CreateAPIView): """ - Представление доступности проверки промокода. + Представление проверки доступности промокода. """ serializer_class = PromocodeCheckSerializer permission_classes = (IsAuthenticated,) From 3b68db6381e379ce9a070e22524f176b4704b48d Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Wed, 1 Nov 2023 04:25:41 +0500 Subject: [PATCH 9/9] del user field --- .../migrations/0008_remove_user_have_orders.py | 17 +++++++++++++++++ api_spot/users/models.py | 4 ---- 2 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 api_spot/users/migrations/0008_remove_user_have_orders.py diff --git a/api_spot/users/migrations/0008_remove_user_have_orders.py b/api_spot/users/migrations/0008_remove_user_have_orders.py new file mode 100644 index 00000000..59540f53 --- /dev/null +++ b/api_spot/users/migrations/0008_remove_user_have_orders.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.5 on 2023-10-31 23:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0007_user_have_orders_user_is_subscribed'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='have_orders', + ), + ] diff --git a/api_spot/users/models.py b/api_spot/users/models.py index c4556687..227fad84 100644 --- a/api_spot/users/models.py +++ b/api_spot/users/models.py @@ -108,10 +108,6 @@ class User(AbstractBaseUser, PermissionsMixin): 'Подписан на рассылку', default=False, ) - have_orders = models.BooleanField( - 'Имеет заказы', - default=False, - ) EMAIL_FIELD = 'email' USERNAME_FIELD = 'email'