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.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-10-01 17:26:07 +03:00
parent 91ed79669b
commit 330177f6e4
17 changed files with 92 additions and 37 deletions

View file

@ -100,7 +100,7 @@ before running installment scripts
### nginx ### nginx
Please comment-out SSL-related lines, then apply necessary configurations, run `certbot --cert-only --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 ### .env

View file

@ -81,6 +81,9 @@ class AmoCRM:
return payload return payload
def _get_customer_name(self, order: Order) -> str: 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"): if not order.attributes.get("business_identificator"):
return ( return (
order.user.get_full_name() order.user.get_full_name()
@ -142,6 +145,8 @@ class AmoCRM:
body = r.json() body = r.json()
return body.get("_embedded", {}).get("contacts", [{}])[0].get("id", None) return body.get("_embedded", {}).get("contacts", [{}])[0].get("id", None)
return None
else: else:
return None return None
@ -180,5 +185,5 @@ class AmoCRM:
if link.order.status == new_status: if link.order.status == new_status:
return return
link.order.status = self.STATUS_MAP.get(new_status) link.order.status = self.STATUS_MAP[new_status]
link.order.save(update_fields=["status"]) link.order.save(update_fields=["status"])

View file

@ -61,6 +61,7 @@ class CaseInsensitiveListFilter(BaseInFilter, CharFilter):
return qs return qs
# noinspection PyUnusedLocal
class ProductFilter(FilterSet): class ProductFilter(FilterSet):
search = CharFilter(field_name="name", method="search_products", label=_("Search")) search = CharFilter(field_name="name", method="search_products", label=_("Search"))
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID")) uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID"))
@ -153,7 +154,7 @@ class ProductFilter(FilterSet):
if not value: if not value:
return queryset 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) return queryset.filter(uuid__in=uuids)
@ -396,6 +397,7 @@ class WishlistFilter(FilterSet):
fields = ["uuid", "user_email", "user", "order_by"] fields = ["uuid", "user_email", "user", "order_by"]
# noinspection PyUnusedLocal
class CategoryFilter(FilterSet): class CategoryFilter(FilterSet):
search = CharFilter(field_name="name", method="search_categories", label=_("Search")) search = CharFilter(field_name="name", method="search_categories", label=_("Search"))
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact") uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
@ -429,7 +431,7 @@ class CategoryFilter(FilterSet):
if not value: if not value:
return queryset 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) return queryset.filter(uuid__in=uuids)
@ -522,6 +524,7 @@ class CategoryFilter(FilterSet):
return queryset.filter(parent__uuid=uuid_val) return queryset.filter(parent__uuid=uuid_val)
# noinspection PyUnusedLocal
class BrandFilter(FilterSet): class BrandFilter(FilterSet):
search = CharFilter(field_name="name", method="search_brands", label=_("Search")) search = CharFilter(field_name="name", method="search_brands", label=_("Search"))
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact") uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
@ -547,7 +550,7 @@ class BrandFilter(FilterSet):
if not value: if not value:
return queryset 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) return queryset.filter(uuid__in=uuids)

View file

@ -546,6 +546,7 @@ class FeedbackProductAction(BaseMutation):
order_product = OrderProduct.objects.get(uuid=order_product_uuid) order_product = OrderProduct.objects.get(uuid=order_product_uuid)
if user != order_product.order.user: if user != order_product.order.user:
raise PermissionDenied(permission_denied_message) raise PermissionDenied(permission_denied_message)
feedback = None
match action: match action:
case "add": case "add":
feedback = order_product.do_feedback(comment=comment, rating=rating, action="add") feedback = order_product.do_feedback(comment=comment, rating=rating, action="add")

View file

@ -1,8 +1,9 @@
import logging import logging
from typing import Any
from constance import config from constance import config
from django.core.cache import cache 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.db.models.functions import Length
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from graphene import ( from graphene import (
@ -55,7 +56,6 @@ from core.utils.seo_builders import (
website_schema, website_schema,
) )
from payments.graphene.object_types import TransactionType from payments.graphene.object_types import TransactionType
from payments.models import Transaction
logger = logging.getLogger("django") logger = logging.getLogger("django")
@ -464,19 +464,19 @@ class OrderType(DjangoObjectType):
) )
description = _("orders") description = _("orders")
def resolve_total_price(self: Order, _info): def resolve_total_price(self: Order, _info) -> float:
return self.total_price return self.total_price
def resolve_total_quantity(self: Order, _info): def resolve_total_quantity(self: Order, _info) -> int:
return self.total_quantity return self.total_quantity
def resolve_notifications(self: Order, _info): def resolve_notifications(self: Order, _info) -> dict[str, Any]:
return camelize(self.notifications) return camelize(self.notifications)
def resolve_attributes(self: Order, _info): def resolve_attributes(self: Order, _info) -> dict[str, Any]:
return camelize(self.attributes) 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: if self.payments_transactions:
return self.payments_transactions.all() return self.payments_transactions.all()
return None return None

View file

@ -9,6 +9,7 @@ def generate_unique_sku(make_candidate, taken):
return c return c
# noinspection PyUnusedLocal
def backfill_sku(apps, schema_editor): def backfill_sku(apps, schema_editor):
Product = apps.get_model("core", "Product") Product = apps.get_model("core", "Product")
from core.utils import generate_human_readable_id as make_candidate 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] last_pk = ids[-1]
# noinspection PyUnusedLocal
def noop(apps, schema_editor): def noop(apps, schema_editor):
pass pass

View file

@ -541,7 +541,7 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): # type: ignore
default=generate_human_readable_id, default=generate_human_readable_id,
) )
objects: ProductManager = ProductManager() objects = ProductManager()
class Meta: class Meta:
verbose_name = _("product") verbose_name = _("product")
@ -1266,6 +1266,10 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
@property @property
def is_business(self) -> bool: def is_business(self) -> bool:
if type(self.attributes) is not dict:
self.attributes = {}
self.save()
return False
with suppress(Exception): with suppress(Exception):
return (self.attributes.get("is_business", False) if self.attributes else False) or ( 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")) (self.user.attributes.get("is_business", False) and self.user.attributes.get("business_identificator"))

View file

@ -106,7 +106,7 @@ class CategoryDetailSerializer(ModelSerializer):
filterable_results = [] filterable_results = []
for attr in attributes: 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 slice_vals = vals[:128] if len(vals) > 128 else vals
filterable_results.append( filterable_results.append(
{ {

View file

@ -51,10 +51,20 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]:
except Order.DoesNotExist: except Order.DoesNotExist:
return False, f"Order not found with the given pk: {order_pk}" 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)]): 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}" 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() set_email_settings()
connection = mail.get_connection() connection = mail.get_connection()
@ -71,7 +81,7 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]:
"total_price": order.total_price, "total_price": order.total_price,
}, },
), ),
to=[order.user.email], to=[recipient],
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>", from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
connection=connection, connection=connection,
) )

View file

@ -235,7 +235,7 @@ class AbstractVendor:
if not rate: if not rate:
raise RatesError(f"No rate found for {currency or self.currency} in {rates} with probider {provider}...") 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 @staticmethod
def round_price_marketologically(price: float) -> float: def round_price_marketologically(price: float) -> float:

View file

@ -428,27 +428,35 @@ class BuyAsBusinessView(APIView):
Handles the "POST" request to process a business purchase. 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): def post(self, request, *_args, **kwargs):
serializer = BuyAsBusinessOrderSerializer(data=request.data) serializer = BuyAsBusinessOrderSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
order = Order.objects.create(status="MOMENTAL") order = Order.objects.create(status="MOMENTAL")
products = [product.get("product_uuid") for product in serializer.validated_data.get("products")] products = [product.get("product_uuid") for product in serializer.validated_data.get("products")]
transaction = order.buy_without_registration( try:
products=products, transaction = order.buy_without_registration(
promocode_uuid=serializer.validated_data.get("promocode_uuid"), products=products,
customer_name=serializer.validated_data.get("business_identificator"), promocode_uuid=serializer.validated_data.get("promocode_uuid"),
customer_email=serializer.validated_data.get("business_email"), customer_name=serializer.validated_data.get("business_identificator"),
customer_phone_number=serializer.validated_data.get("business_phone_number"), customer_email=serializer.validated_data.get("business_email"),
billing_customer_address=serializer.validated_data.get("billing_business_address_uuid"), customer_phone_number=serializer.validated_data.get("business_phone_number"),
shipping_customer_address=serializer.validated_data.get("shipping_business_address_uuid"), billing_customer_address=serializer.validated_data.get("billing_business_address_uuid"),
payment_method=serializer.validated_data.get("payment_method"), shipping_customer_address=serializer.validated_data.get("shipping_business_address_uuid"),
is_business=True, payment_method=serializer.validated_data.get("payment_method"),
) is_business=True,
return Response( )
status=status.HTTP_201_CREATED, return Response(
data=TransactionProcessSerializer(transaction).data, 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): def download_digital_asset_view(request, *args, **kwargs):

View file

@ -155,6 +155,7 @@ class EvibesViewSet(ModelViewSet):
@extend_schema_view(**ATTRIBUTE_GROUP_SCHEMA) @extend_schema_view(**ATTRIBUTE_GROUP_SCHEMA)
# noinspection PyUnusedLocal
class AttributeGroupViewSet(EvibesViewSet): class AttributeGroupViewSet(EvibesViewSet):
""" """
Represents a viewset for managing AttributeGroup objects. Represents a viewset for managing AttributeGroup objects.
@ -187,6 +188,7 @@ class AttributeGroupViewSet(EvibesViewSet):
@extend_schema_view(**ATTRIBUTE_SCHEMA) @extend_schema_view(**ATTRIBUTE_SCHEMA)
# noinspection PyUnusedLocal
class AttributeViewSet(EvibesViewSet): class AttributeViewSet(EvibesViewSet):
""" """
Handles operations related to Attribute objects within the application. Handles operations related to Attribute objects within the application.
@ -219,6 +221,7 @@ class AttributeViewSet(EvibesViewSet):
@extend_schema_view(**ATTRIBUTE_VALUE_SCHEMA) @extend_schema_view(**ATTRIBUTE_VALUE_SCHEMA)
# noinspection PyUnusedLocal
class AttributeValueViewSet(EvibesViewSet): class AttributeValueViewSet(EvibesViewSet):
""" """
A viewset for managing AttributeValue objects. A viewset for managing AttributeValue objects.
@ -247,6 +250,7 @@ class AttributeValueViewSet(EvibesViewSet):
@extend_schema_view(**CATEGORY_SCHEMA) @extend_schema_view(**CATEGORY_SCHEMA)
# noinspection PyUnusedLocal
class CategoryViewSet(EvibesViewSet): class CategoryViewSet(EvibesViewSet):
""" """
Manages views for Category-related operations. Manages views for Category-related operations.
@ -377,6 +381,7 @@ class CategoryViewSet(EvibesViewSet):
return Response(SeoSnapshotSerializer(payload).data) return Response(SeoSnapshotSerializer(payload).data)
# noinspection PyUnusedLocal
class BrandViewSet(EvibesViewSet): class BrandViewSet(EvibesViewSet):
""" """
Represents a viewset for managing Brand instances. Represents a viewset for managing Brand instances.
@ -502,6 +507,7 @@ class BrandViewSet(EvibesViewSet):
@extend_schema_view(**PRODUCT_SCHEMA) @extend_schema_view(**PRODUCT_SCHEMA)
# noinspection PyUnusedLocal
class ProductViewSet(EvibesViewSet): class ProductViewSet(EvibesViewSet):
""" """
Manages operations related to the `Product` model in the system. Manages operations related to the `Product` model in the system.
@ -635,6 +641,7 @@ class ProductViewSet(EvibesViewSet):
return Response(SeoSnapshotSerializer(payload).data) return Response(SeoSnapshotSerializer(payload).data)
# noinspection PyUnusedLocal
class VendorViewSet(EvibesViewSet): class VendorViewSet(EvibesViewSet):
""" """
Represents a viewset for managing Vendor objects. Represents a viewset for managing Vendor objects.
@ -666,6 +673,7 @@ class VendorViewSet(EvibesViewSet):
@extend_schema_view(**FEEDBACK_SCHEMA) @extend_schema_view(**FEEDBACK_SCHEMA)
# noinspection PyUnusedLocal
class FeedbackViewSet(EvibesViewSet): class FeedbackViewSet(EvibesViewSet):
""" """
Representation of a view set handling Feedback objects. Representation of a view set handling Feedback objects.
@ -704,6 +712,7 @@ class FeedbackViewSet(EvibesViewSet):
@extend_schema_view(**ORDER_SCHEMA) @extend_schema_view(**ORDER_SCHEMA)
# noinspection PyUnusedLocal
class OrderViewSet(EvibesViewSet): class OrderViewSet(EvibesViewSet):
""" """
ViewSet for managing orders and related operations. ViewSet for managing orders and related operations.
@ -921,6 +930,7 @@ class OrderViewSet(EvibesViewSet):
@extend_schema_view(**ORDER_PRODUCT_SCHEMA) @extend_schema_view(**ORDER_PRODUCT_SCHEMA)
# noinspection PyUnusedLocal
class OrderProductViewSet(EvibesViewSet): class OrderProductViewSet(EvibesViewSet):
""" """
Provides a viewset for managing OrderProduct entities. Provides a viewset for managing OrderProduct entities.
@ -993,6 +1003,7 @@ class OrderProductViewSet(EvibesViewSet):
return Response(status=status.HTTP_404_NOT_FOUND) return Response(status=status.HTTP_404_NOT_FOUND)
# noinspection PyUnusedLocal
class ProductImageViewSet(EvibesViewSet): class ProductImageViewSet(EvibesViewSet):
""" """
Manages operations related to Product images in the application. Manages operations related to Product images in the application.
@ -1025,6 +1036,7 @@ class ProductImageViewSet(EvibesViewSet):
} }
# noinspection PyUnusedLocal
class PromoCodeViewSet(EvibesViewSet): class PromoCodeViewSet(EvibesViewSet):
""" """
Manages the retrieval and handling of PromoCode instances through various Manages the retrieval and handling of PromoCode instances through various
@ -1064,6 +1076,7 @@ class PromoCodeViewSet(EvibesViewSet):
return qs.filter(user=user) return qs.filter(user=user)
# noinspection PyUnusedLocal
class PromotionViewSet(EvibesViewSet): class PromotionViewSet(EvibesViewSet):
""" """
Represents a view set for managing promotions. Represents a view set for managing promotions.
@ -1083,6 +1096,7 @@ class PromotionViewSet(EvibesViewSet):
} }
# noinspection PyUnusedLocal
class StockViewSet(EvibesViewSet): class StockViewSet(EvibesViewSet):
""" """
Handles operations related to Stock data in the system. Handles operations related to Stock data in the system.
@ -1115,6 +1129,7 @@ class StockViewSet(EvibesViewSet):
@extend_schema_view(**WISHLIST_SCHEMA) @extend_schema_view(**WISHLIST_SCHEMA)
# noinspection PyUnusedLocal
class WishlistViewSet(EvibesViewSet): class WishlistViewSet(EvibesViewSet):
""" """
ViewSet for managing Wishlist operations. ViewSet for managing Wishlist operations.
@ -1254,6 +1269,7 @@ class WishlistViewSet(EvibesViewSet):
@extend_schema_view(**ADDRESS_SCHEMA) @extend_schema_view(**ADDRESS_SCHEMA)
# noinspection PyUnusedLocal
class AddressViewSet(EvibesViewSet): class AddressViewSet(EvibesViewSet):
""" """
This class provides viewset functionality for managing `Address` objects. This class provides viewset functionality for managing `Address` objects.
@ -1329,6 +1345,7 @@ class AddressViewSet(EvibesViewSet):
) )
# noinspection PyUnusedLocal
class ProductTagViewSet(EvibesViewSet): class ProductTagViewSet(EvibesViewSet):
""" """
Handles operations related to Product Tags within the application. Handles operations related to Product Tags within the application.

View file

@ -5,6 +5,7 @@ from storages.backends.ftp import FTPStorage
class AbsoluteFTPStorage(FTPStorage): # type: ignore class AbsoluteFTPStorage(FTPStorage): # type: ignore
# noinspection PyProtectedMember # noinspection PyProtectedMember
# noinspection PyUnresolvedReferences
def _get_config(self): def _get_config(self):
cfg = super()._get_config() cfg = super()._get_config()

View file

@ -1,5 +1,4 @@
import graphene import graphene
from django.db.models import QuerySet
from graphene import relay from graphene import relay
from graphene.types.generic import GenericScalar from graphene.types.generic import GenericScalar
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
@ -32,7 +31,7 @@ class BalanceType(DjangoObjectType):
interfaces = (relay.Node,) interfaces = (relay.Node,)
filter_fields = ["is_active"] 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: if info.context.user == self.user:
# noinspection Mypy # noinspection Mypy
return self.transactions.all() or [] return self.transactions.all() or []

View file

@ -22,8 +22,11 @@ class Transaction(NiceModel):
process = JSONField(verbose_name=_("processing details"), default=dict) process = JSONField(verbose_name=_("processing details"), default=dict)
def __str__(self): def __str__(self):
return f"{self.balance.user.email} | {self.amount}" if self.balance else\ return (
f"{self.order.attributes.get("customer_email")} | {self.amount}" f"{self.balance.user.email} | {self.amount}"
if self.balance
else f"{self.order.attributes.get('customer_email')} | {self.amount}"
)
def save(self, **kwargs): def save(self, **kwargs):
if self.amount != 0.0 and ( if self.amount != 0.0 and (

View file

@ -22,6 +22,7 @@ def create_balance_on_user_creation_signal(instance, created, **_kwargs):
def process_transaction_changes(instance, created, **_kwargs): def process_transaction_changes(instance, created, **_kwargs):
if created: if created:
try: try:
gateway = None
match instance.process.get("gateway", "default"): match instance.process.get("gateway", "default"):
case "gateway": case "gateway":
gateway = AbstractGateway() gateway = AbstractGateway()

View file

@ -30,6 +30,7 @@ from vibes_auth.utils.emailing import send_reset_password_email_task
logger = logging.getLogger("django") logger = logging.getLogger("django")
# noinspection GrazieInspection
@extend_schema_view(**USER_SCHEMA) @extend_schema_view(**USER_SCHEMA)
class UserViewSet( class UserViewSet(
mixins.CreateModelMixin, mixins.CreateModelMixin,