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:
parent
91ed79669b
commit
330177f6e4
17 changed files with 92 additions and 37 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
2
core/vendors/__init__.py
vendored
2
core/vendors/__init__.py
vendored
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue