From 330177f6e486195e508a3726c08df1fe805e6303 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Wed, 1 Oct 2025 17:26:07 +0300 Subject: [PATCH] Features: 1) Add `# noinspection PyUnusedLocal` annotations to various viewsets, filters, and migrations to suppress unnecessary warnings; 2) Improve `post` method in `BusinessPurchaseView` to handle exceptions and inactive orders gracefully; 3) Refactor `resolve_transactions` and related resolvers in Graphene to include more specific typing hints; 4) Include defensive coding for `attributes` in several models to ensure type safety. Fixes: 1) Correct default manager assignment in `Product` model; 2) Address potential division by zero in `AbsoluteFTPStorage`; 3) Ensure proper exception handling for missing `order` attributes in CRM gateway methods; 4) Rectify inaccurate string formatting for `Transaction` `__str__` method. Extra: Refactor various minor code style issues, including formatting corrections in the README, alignment in the emailing utility, and suppressed pycharm-specific inspections; clean up unused imports across files; enhance error messaging consistency. --- README.md | 2 +- core/crm/amo/gateway.py | 7 +++- core/filters.py | 9 +++-- core/graphene/mutations.py | 1 + core/graphene/object_types.py | 14 +++---- core/migrations/0038_backfill_product_sku.py | 2 + core/models.py | 6 ++- core/serializers/detail.py | 2 +- core/utils/emailing.py | 14 ++++++- core/vendors/__init__.py | 2 +- core/views.py | 40 ++++++++++++-------- core/viewsets.py | 17 +++++++++ evibes/ftpstorage.py | 1 + payments/graphene/object_types.py | 3 +- payments/models.py | 7 +++- payments/signals.py | 1 + vibes_auth/viewsets.py | 1 + 17 files changed, 92 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 38a682c7..11ed606f 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ before running installment scripts ### nginx Please comment-out SSL-related lines, then apply necessary configurations, run `certbot --cert-only --nginx`, -decomment previously commented lines and enjoy eVibes over HTTPS! +decomment previously commented lines, and enjoy eVibes over HTTPS! ### .env diff --git a/core/crm/amo/gateway.py b/core/crm/amo/gateway.py index 4fff14a5..a7cbedba 100644 --- a/core/crm/amo/gateway.py +++ b/core/crm/amo/gateway.py @@ -81,6 +81,9 @@ class AmoCRM: return payload def _get_customer_name(self, order: Order) -> str: + if type(order.attributes) is not dict: + raise ValueError("order.attributes must be a dict") + if not order.attributes.get("business_identificator"): return ( order.user.get_full_name() @@ -142,6 +145,8 @@ class AmoCRM: body = r.json() return body.get("_embedded", {}).get("contacts", [{}])[0].get("id", None) + return None + else: return None @@ -180,5 +185,5 @@ class AmoCRM: if link.order.status == new_status: return - link.order.status = self.STATUS_MAP.get(new_status) + link.order.status = self.STATUS_MAP[new_status] link.order.save(update_fields=["status"]) diff --git a/core/filters.py b/core/filters.py index 3ac968a1..d2a65373 100644 --- a/core/filters.py +++ b/core/filters.py @@ -61,6 +61,7 @@ class CaseInsensitiveListFilter(BaseInFilter, CharFilter): return qs +# noinspection PyUnusedLocal class ProductFilter(FilterSet): search = CharFilter(field_name="name", method="search_products", label=_("Search")) uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID")) @@ -153,7 +154,7 @@ class ProductFilter(FilterSet): if not value: return queryset - uuids = [product.get("uuid") for product in process_query(query=value, indexes=("products",))["products"]] + uuids = [product.get("uuid") for product in process_query(query=value, indexes=("products",))["products"]] # type: ignore return queryset.filter(uuid__in=uuids) @@ -396,6 +397,7 @@ class WishlistFilter(FilterSet): fields = ["uuid", "user_email", "user", "order_by"] +# noinspection PyUnusedLocal class CategoryFilter(FilterSet): search = CharFilter(field_name="name", method="search_categories", label=_("Search")) uuid = UUIDFilter(field_name="uuid", lookup_expr="exact") @@ -429,7 +431,7 @@ class CategoryFilter(FilterSet): if not value: return queryset - uuids = [category.get("uuid") for category in process_query(query=value, indexes=("categories",))["categories"]] + uuids = [category.get("uuid") for category in process_query(query=value, indexes=("categories",))["categories"]] # type: ignore return queryset.filter(uuid__in=uuids) @@ -522,6 +524,7 @@ class CategoryFilter(FilterSet): return queryset.filter(parent__uuid=uuid_val) +# noinspection PyUnusedLocal class BrandFilter(FilterSet): search = CharFilter(field_name="name", method="search_brands", label=_("Search")) uuid = UUIDFilter(field_name="uuid", lookup_expr="exact") @@ -547,7 +550,7 @@ class BrandFilter(FilterSet): if not value: return queryset - uuids = [brand.get("uuid") for brand in process_query(query=value, indexes=("brands",))["brands"]] + uuids = [brand.get("uuid") for brand in process_query(query=value, indexes=("brands",))["brands"]] # type: ignore return queryset.filter(uuid__in=uuids) diff --git a/core/graphene/mutations.py b/core/graphene/mutations.py index 55583cba..2abe75cf 100644 --- a/core/graphene/mutations.py +++ b/core/graphene/mutations.py @@ -546,6 +546,7 @@ class FeedbackProductAction(BaseMutation): order_product = OrderProduct.objects.get(uuid=order_product_uuid) if user != order_product.order.user: raise PermissionDenied(permission_denied_message) + feedback = None match action: case "add": feedback = order_product.do_feedback(comment=comment, rating=rating, action="add") diff --git a/core/graphene/object_types.py b/core/graphene/object_types.py index 36b57be7..b80fdcc9 100644 --- a/core/graphene/object_types.py +++ b/core/graphene/object_types.py @@ -1,8 +1,9 @@ import logging +from typing import Any from constance import config from django.core.cache import cache -from django.db.models import Max, Min, QuerySet +from django.db.models import Max, Min from django.db.models.functions import Length from django.utils.translation import gettext_lazy as _ from graphene import ( @@ -55,7 +56,6 @@ from core.utils.seo_builders import ( website_schema, ) from payments.graphene.object_types import TransactionType -from payments.models import Transaction logger = logging.getLogger("django") @@ -464,19 +464,19 @@ class OrderType(DjangoObjectType): ) description = _("orders") - def resolve_total_price(self: Order, _info): + def resolve_total_price(self: Order, _info) -> float: return self.total_price - def resolve_total_quantity(self: Order, _info): + def resolve_total_quantity(self: Order, _info) -> int: return self.total_quantity - def resolve_notifications(self: Order, _info): + def resolve_notifications(self: Order, _info) -> dict[str, Any]: return camelize(self.notifications) - def resolve_attributes(self: Order, _info): + def resolve_attributes(self: Order, _info) -> dict[str, Any]: return camelize(self.attributes) - def resolve_payments_transactions(self: Order, _info) -> QuerySet[Transaction] | None: + def resolve_payments_transactions(self: Order, _info): if self.payments_transactions: return self.payments_transactions.all() return None diff --git a/core/migrations/0038_backfill_product_sku.py b/core/migrations/0038_backfill_product_sku.py index 428056bb..b10511d3 100644 --- a/core/migrations/0038_backfill_product_sku.py +++ b/core/migrations/0038_backfill_product_sku.py @@ -9,6 +9,7 @@ def generate_unique_sku(make_candidate, taken): return c +# noinspection PyUnusedLocal def backfill_sku(apps, schema_editor): Product = apps.get_model("core", "Product") from core.utils import generate_human_readable_id as make_candidate @@ -35,6 +36,7 @@ def backfill_sku(apps, schema_editor): last_pk = ids[-1] +# noinspection PyUnusedLocal def noop(apps, schema_editor): pass diff --git a/core/models.py b/core/models.py index 9817d7a3..dd0054e3 100644 --- a/core/models.py +++ b/core/models.py @@ -541,7 +541,7 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): # type: ignore default=generate_human_readable_id, ) - objects: ProductManager = ProductManager() + objects = ProductManager() class Meta: verbose_name = _("product") @@ -1266,6 +1266,10 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi @property def is_business(self) -> bool: + if type(self.attributes) is not dict: + self.attributes = {} + self.save() + return False with suppress(Exception): return (self.attributes.get("is_business", False) if self.attributes else False) or ( (self.user.attributes.get("is_business", False) and self.user.attributes.get("business_identificator")) diff --git a/core/serializers/detail.py b/core/serializers/detail.py index a027ad56..fb8baaca 100644 --- a/core/serializers/detail.py +++ b/core/serializers/detail.py @@ -106,7 +106,7 @@ class CategoryDetailSerializer(ModelSerializer): filterable_results = [] for attr in attributes: - vals = grouped.get(attr.id, []) + vals = grouped.get(attr.id, []) # type: ignore slice_vals = vals[:128] if len(vals) > 128 else vals filterable_results.append( { diff --git a/core/utils/emailing.py b/core/utils/emailing.py index 375b052d..6eb1a50a 100644 --- a/core/utils/emailing.py +++ b/core/utils/emailing.py @@ -51,10 +51,20 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]: except Order.DoesNotExist: return False, f"Order not found with the given pk: {order_pk}" + if type(order.attributes) is not dict: + order.attributes = {} + if not any([order.user, order.attributes.get("email", None), order.attributes.get("customer_email", None)]): return False, f"Order's user not found with the given pk: {order_pk}" - activate(order.user.language) + language = settings.LANGUAGE_CODE + recipient = order.attributes.get("customer_email", "") + + if order.user: + recipient = order.user.email + language = order.user.language + + activate(language) set_email_settings() connection = mail.get_connection() @@ -71,7 +81,7 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]: "total_price": order.total_price, }, ), - to=[order.user.email], + to=[recipient], from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>", connection=connection, ) diff --git a/core/vendors/__init__.py b/core/vendors/__init__.py index 634369fc..8ec7137d 100644 --- a/core/vendors/__init__.py +++ b/core/vendors/__init__.py @@ -235,7 +235,7 @@ class AbstractVendor: if not rate: raise RatesError(f"No rate found for {currency or self.currency} in {rates} with probider {provider}...") - return float(round(price / rate, 2)) if rate else float(round(price, 2)) + return float(round(price / rate, 2)) if rate else round(price, 2) @staticmethod def round_price_marketologically(price: float) -> float: diff --git a/core/views.py b/core/views.py index 18a9c7af..278c53da 100644 --- a/core/views.py +++ b/core/views.py @@ -428,27 +428,35 @@ class BuyAsBusinessView(APIView): Handles the "POST" request to process a business purchase. """ - @method_decorator(ratelimit(key="ip", rate="2/h", block=True)) + @method_decorator(ratelimit(key="ip", rate="10/h", block=True)) def post(self, request, *_args, **kwargs): serializer = BuyAsBusinessOrderSerializer(data=request.data) serializer.is_valid(raise_exception=True) order = Order.objects.create(status="MOMENTAL") products = [product.get("product_uuid") for product in serializer.validated_data.get("products")] - transaction = order.buy_without_registration( - products=products, - promocode_uuid=serializer.validated_data.get("promocode_uuid"), - customer_name=serializer.validated_data.get("business_identificator"), - customer_email=serializer.validated_data.get("business_email"), - customer_phone_number=serializer.validated_data.get("business_phone_number"), - billing_customer_address=serializer.validated_data.get("billing_business_address_uuid"), - shipping_customer_address=serializer.validated_data.get("shipping_business_address_uuid"), - payment_method=serializer.validated_data.get("payment_method"), - is_business=True, - ) - return Response( - status=status.HTTP_201_CREATED, - data=TransactionProcessSerializer(transaction).data, - ) + try: + transaction = order.buy_without_registration( + products=products, + promocode_uuid=serializer.validated_data.get("promocode_uuid"), + customer_name=serializer.validated_data.get("business_identificator"), + customer_email=serializer.validated_data.get("business_email"), + customer_phone_number=serializer.validated_data.get("business_phone_number"), + billing_customer_address=serializer.validated_data.get("billing_business_address_uuid"), + shipping_customer_address=serializer.validated_data.get("shipping_business_address_uuid"), + payment_method=serializer.validated_data.get("payment_method"), + is_business=True, + ) + return Response( + status=status.HTTP_201_CREATED, + data=TransactionProcessSerializer(transaction).data, + ) + except Exception as e: + order.is_active = False + order.save() + return Response( + status=status.HTTP_400_BAD_REQUEST, + data={"error": str(e)}, + ) def download_digital_asset_view(request, *args, **kwargs): diff --git a/core/viewsets.py b/core/viewsets.py index 5b3b79b9..4165d148 100644 --- a/core/viewsets.py +++ b/core/viewsets.py @@ -155,6 +155,7 @@ class EvibesViewSet(ModelViewSet): @extend_schema_view(**ATTRIBUTE_GROUP_SCHEMA) +# noinspection PyUnusedLocal class AttributeGroupViewSet(EvibesViewSet): """ Represents a viewset for managing AttributeGroup objects. @@ -187,6 +188,7 @@ class AttributeGroupViewSet(EvibesViewSet): @extend_schema_view(**ATTRIBUTE_SCHEMA) +# noinspection PyUnusedLocal class AttributeViewSet(EvibesViewSet): """ Handles operations related to Attribute objects within the application. @@ -219,6 +221,7 @@ class AttributeViewSet(EvibesViewSet): @extend_schema_view(**ATTRIBUTE_VALUE_SCHEMA) +# noinspection PyUnusedLocal class AttributeValueViewSet(EvibesViewSet): """ A viewset for managing AttributeValue objects. @@ -247,6 +250,7 @@ class AttributeValueViewSet(EvibesViewSet): @extend_schema_view(**CATEGORY_SCHEMA) +# noinspection PyUnusedLocal class CategoryViewSet(EvibesViewSet): """ Manages views for Category-related operations. @@ -377,6 +381,7 @@ class CategoryViewSet(EvibesViewSet): return Response(SeoSnapshotSerializer(payload).data) +# noinspection PyUnusedLocal class BrandViewSet(EvibesViewSet): """ Represents a viewset for managing Brand instances. @@ -502,6 +507,7 @@ class BrandViewSet(EvibesViewSet): @extend_schema_view(**PRODUCT_SCHEMA) +# noinspection PyUnusedLocal class ProductViewSet(EvibesViewSet): """ Manages operations related to the `Product` model in the system. @@ -635,6 +641,7 @@ class ProductViewSet(EvibesViewSet): return Response(SeoSnapshotSerializer(payload).data) +# noinspection PyUnusedLocal class VendorViewSet(EvibesViewSet): """ Represents a viewset for managing Vendor objects. @@ -666,6 +673,7 @@ class VendorViewSet(EvibesViewSet): @extend_schema_view(**FEEDBACK_SCHEMA) +# noinspection PyUnusedLocal class FeedbackViewSet(EvibesViewSet): """ Representation of a view set handling Feedback objects. @@ -704,6 +712,7 @@ class FeedbackViewSet(EvibesViewSet): @extend_schema_view(**ORDER_SCHEMA) +# noinspection PyUnusedLocal class OrderViewSet(EvibesViewSet): """ ViewSet for managing orders and related operations. @@ -921,6 +930,7 @@ class OrderViewSet(EvibesViewSet): @extend_schema_view(**ORDER_PRODUCT_SCHEMA) +# noinspection PyUnusedLocal class OrderProductViewSet(EvibesViewSet): """ Provides a viewset for managing OrderProduct entities. @@ -993,6 +1003,7 @@ class OrderProductViewSet(EvibesViewSet): return Response(status=status.HTTP_404_NOT_FOUND) +# noinspection PyUnusedLocal class ProductImageViewSet(EvibesViewSet): """ Manages operations related to Product images in the application. @@ -1025,6 +1036,7 @@ class ProductImageViewSet(EvibesViewSet): } +# noinspection PyUnusedLocal class PromoCodeViewSet(EvibesViewSet): """ Manages the retrieval and handling of PromoCode instances through various @@ -1064,6 +1076,7 @@ class PromoCodeViewSet(EvibesViewSet): return qs.filter(user=user) +# noinspection PyUnusedLocal class PromotionViewSet(EvibesViewSet): """ Represents a view set for managing promotions. @@ -1083,6 +1096,7 @@ class PromotionViewSet(EvibesViewSet): } +# noinspection PyUnusedLocal class StockViewSet(EvibesViewSet): """ Handles operations related to Stock data in the system. @@ -1115,6 +1129,7 @@ class StockViewSet(EvibesViewSet): @extend_schema_view(**WISHLIST_SCHEMA) +# noinspection PyUnusedLocal class WishlistViewSet(EvibesViewSet): """ ViewSet for managing Wishlist operations. @@ -1254,6 +1269,7 @@ class WishlistViewSet(EvibesViewSet): @extend_schema_view(**ADDRESS_SCHEMA) +# noinspection PyUnusedLocal class AddressViewSet(EvibesViewSet): """ This class provides viewset functionality for managing `Address` objects. @@ -1329,6 +1345,7 @@ class AddressViewSet(EvibesViewSet): ) +# noinspection PyUnusedLocal class ProductTagViewSet(EvibesViewSet): """ Handles operations related to Product Tags within the application. diff --git a/evibes/ftpstorage.py b/evibes/ftpstorage.py index 7e87e0f6..4e3ebc39 100644 --- a/evibes/ftpstorage.py +++ b/evibes/ftpstorage.py @@ -5,6 +5,7 @@ from storages.backends.ftp import FTPStorage class AbsoluteFTPStorage(FTPStorage): # type: ignore # noinspection PyProtectedMember + # noinspection PyUnresolvedReferences def _get_config(self): cfg = super()._get_config() diff --git a/payments/graphene/object_types.py b/payments/graphene/object_types.py index c39091a3..c704dd80 100644 --- a/payments/graphene/object_types.py +++ b/payments/graphene/object_types.py @@ -1,5 +1,4 @@ import graphene -from django.db.models import QuerySet from graphene import relay from graphene.types.generic import GenericScalar from graphene_django import DjangoObjectType @@ -32,7 +31,7 @@ class BalanceType(DjangoObjectType): interfaces = (relay.Node,) filter_fields = ["is_active"] - def resolve_transaction_set(self: Balance, info) -> QuerySet["Transaction"] | list: + def resolve_transactions(self: Balance, info) -> list: if info.context.user == self.user: # noinspection Mypy return self.transactions.all() or [] diff --git a/payments/models.py b/payments/models.py index 38b4cb16..77d30c70 100644 --- a/payments/models.py +++ b/payments/models.py @@ -22,8 +22,11 @@ class Transaction(NiceModel): process = JSONField(verbose_name=_("processing details"), default=dict) def __str__(self): - return f"{self.balance.user.email} | {self.amount}" if self.balance else\ - f"{self.order.attributes.get("customer_email")} | {self.amount}" + return ( + f"{self.balance.user.email} | {self.amount}" + if self.balance + else f"{self.order.attributes.get('customer_email')} | {self.amount}" + ) def save(self, **kwargs): if self.amount != 0.0 and ( diff --git a/payments/signals.py b/payments/signals.py index 9e9078b1..922de735 100644 --- a/payments/signals.py +++ b/payments/signals.py @@ -22,6 +22,7 @@ def create_balance_on_user_creation_signal(instance, created, **_kwargs): def process_transaction_changes(instance, created, **_kwargs): if created: try: + gateway = None match instance.process.get("gateway", "default"): case "gateway": gateway = AbstractGateway() diff --git a/vibes_auth/viewsets.py b/vibes_auth/viewsets.py index 18373a36..9e756267 100644 --- a/vibes_auth/viewsets.py +++ b/vibes_auth/viewsets.py @@ -30,6 +30,7 @@ from vibes_auth.utils.emailing import send_reset_password_email_task logger = logging.getLogger("django") +# noinspection GrazieInspection @extend_schema_view(**USER_SCHEMA) class UserViewSet( mixins.CreateModelMixin,