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
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

View file

@ -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"])

View file

@ -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)

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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"))

View file

@ -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(
{

View file

@ -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,
)

View file

@ -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:

View file

@ -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):

View file

@ -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.

View file

@ -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()

View file

@ -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 []

View file

@ -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 (

View file

@ -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()

View file

@ -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,