From 09366213b65d6fb64eb88f374d6bc00e68e9ea2b Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Sat, 12 Jul 2025 18:49:25 +0300 Subject: [PATCH] Features: 1) Introduced `AddressFilter` and `AddressType` to enable filtering and querying addresses via GraphQL; 2) Added `resolve_addresses` method with permission check in GraphQL schema; 3) Updated DRF settings to improve API documentation structure. Fixes: 1) Corrected `apply_addresses` logic to handle address validation more robustly; 2) Fixed incorrect imports and annotations for better type safety; 3) Resolved typos and clarified docstrings for various views and methods. Extra: Adjusted formatting, added `# type: ignore` for stricter type checks, and removed unused `dist/` directory from `.dockerignore`. --- .dockerignore | 1 - core/graphene/schema.py | 15 +++++++-- core/models.py | 19 ++++++----- core/vendors/__init__.py | 6 ++-- core/views.py | 2 +- evibes/settings/drf.py | 49 ++++++++++++++++++----------- evibes/settings/extensions.py | 1 + vibes_auth/graphene/object_types.py | 3 +- vibes_auth/views.py | 6 ++-- vibes_auth/viewsets.py | 2 +- 10 files changed, 62 insertions(+), 42 deletions(-) diff --git a/.dockerignore b/.dockerignore index f8d01a3f..19f7e1bb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -38,7 +38,6 @@ __pypackages__/ # Packaging and distribution # ────────────────────────────────────────────────────────────────────────── build/ -dist/ dist-ssr/ *.egg *.egg-info/ diff --git a/core/graphene/schema.py b/core/graphene/schema.py index 4b447f7d..046f695d 100644 --- a/core/graphene/schema.py +++ b/core/graphene/schema.py @@ -1,6 +1,7 @@ import logging from django.core.cache import cache +from django.utils import timezone from django.core.exceptions import PermissionDenied from graphene import Field, List, ObjectType, Schema from graphene_django.filter import DjangoFilterConnectionField @@ -14,6 +15,7 @@ from core.filters import ( OrderFilter, ProductFilter, WishlistFilter, + AddressFilter, ) from core.graphene.mutations import ( AddOrderProduct, @@ -58,6 +60,7 @@ from core.graphene.object_types import ( StockType, VendorType, WishlistType, + AddressType, ) from core.models import ( AttributeGroup, @@ -75,6 +78,7 @@ from core.models import ( Stock, Vendor, Wishlist, + Address, ) from core.utils import get_project_parameters from core.utils.languages import get_flag_by_language @@ -106,6 +110,7 @@ class Query(ObjectType): products = DjangoFilterConnectionField(ProductType, filterset_class=ProductFilter) orders = DjangoFilterConnectionField(OrderType, filterset_class=OrderFilter) users = DjangoFilterConnectionField(UserType, filterset_class=UserFilter) + addresses = DjangoFilterConnectionField(AddressType, filterset_class=AddressFilter) attribute_groups = DjangoFilterConnectionField(AttributeGroupType) categories = DjangoFilterConnectionField(CategoryType, filterset_class=CategoryFilter) vendors = DjangoFilterConnectionField(VendorType) @@ -294,8 +299,8 @@ class Query(ObjectType): is_active=True, user=info.context.user, used_on__isnull=True, - start_time__lte=info.context.now, - ) + start_time__lte=timezone.now(), + ) @staticmethod def resolve_product_tags(_parent, info, **_kwargs): @@ -309,6 +314,12 @@ class Query(ObjectType): return CategoryTag.objects.all() return CategoryTag.objects.filter(is_active=True) + @staticmethod + def resolve_addresses(_parent, info, **_kwargs): + if info.context.user.has_perm("core.view_address"): + return Address.objects.all() + return Address.objects.filter(is_active=True, user=info.context.user) + class Mutation(ObjectType): search = Search.Field() diff --git a/core/models.py b/core/models.py index 00ab49fc..45c4c6e2 100644 --- a/core/models.py +++ b/core/models.py @@ -1,7 +1,7 @@ import datetime import json import logging -from typing import Optional, Self, Any +from typing import Any, Optional, Self from constance import config from django.contrib.gis.db.models import PointField @@ -28,8 +28,8 @@ from django.db.models import ( Max, OneToOneField, PositiveIntegerField, - TextField, QuerySet, + TextField, ) from django.db.models.indexes import Index from django.http import Http404 @@ -1536,13 +1536,12 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi raise Http404(_("promocode does not exist")) from dne return promocode.use(self) - def apply_addresses(self, billing_address_uuid: str | None = None, shipping_address_uuid: str | None = None): + def apply_addresses( + self, billing_address_uuid: str | None = None, shipping_address_uuid: str | None = None + ): try: - if not any([shipping_address_uuid, billing_address_uuid]): - if self.is_whole_digital: - return - else: - raise ValueError(_("you can only buy physical products with shipping address specified")) + if not any([shipping_address_uuid, billing_address_uuid]) and not self.is_whole_digital: + raise ValueError(_("you can only buy physical products with shipping address specified")) if billing_address_uuid and not shipping_address_uuid: shipping_address = Address.objects.get(uuid=billing_address_uuid) @@ -1553,8 +1552,8 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi shipping_address = billing_address else: - billing_address = Address.objects.get(uuid=billing_address_uuid) - shipping_address = Address.objects.get(uuid=shipping_address_uuid) + billing_address = Address.objects.get(uuid=billing_address_uuid) # type: ignore [misc] + shipping_address = Address.objects.get(uuid=shipping_address_uuid) # type: ignore [misc] self.billing_address = billing_address self.shipping_address = shipping_address diff --git a/core/vendors/__init__.py b/core/vendors/__init__.py index 519a9915..413b5320 100644 --- a/core/vendors/__init__.py +++ b/core/vendors/__init__.py @@ -301,13 +301,11 @@ class AbstractVendor: raise ValueError(f"Invalid method {method!r} for products update...") def delete_inactives(self, inactivation_method: str = "deactivate", size: int = 5000): - filter_kwargs: dict[str, Any] = {} - # noinspection PyUnreachableCode match inactivation_method: case "deactivate": - filter_kwargs = {"is_active": False} + filter_kwargs: dict[str, Any] = {"is_active": False} case "description": - filter_kwargs = {"description__exact": "EVIBES_DELETED_PRODUCT"} + filter_kwargs: dict[str, Any] = {"description__exact": "EVIBES_DELETED_PRODUCT"} case _: raise ValueError(f"Invalid method {inactivation_method!r} for products cleaner...") diff --git a/core/views.py b/core/views.py index 79505358..a2c2647f 100644 --- a/core/views.py +++ b/core/views.py @@ -421,7 +421,7 @@ class BuyAsBusinessView(APIView): Methods: post(request, *_args, **kwargs): - Handles the post request to process a business purchase. + Handles the "POST" request to process a business purchase. """ @ratelimit(key="ip", rate="2/h", block=True) diff --git a/evibes/settings/drf.py b/evibes/settings/drf.py index fee15a33..230788c3 100644 --- a/evibes/settings/drf.py +++ b/evibes/settings/drf.py @@ -45,11 +45,15 @@ SIMPLE_JWT: dict[str, timedelta | str | bool] = { "AUTH_HEADER_NAME": "HTTP_X_EVIBES_AUTH", } -# type: ignore -SPECTACULAR_B2B_DESCRIPTION = _(f""" -Welcome to the {CONSTANCE_CONFIG.get("PROJECT_NAME")[0]} B2B API documentation. +SPECTACULAR_B2B_DESCRIPTION = _( # type: ignore [index] + f""" +Welcome to the { + CONSTANCE_CONFIG.get("PROJECT_NAME")[0] # type: ignore [index] + } B2B API documentation. -The {CONSTANCE_CONFIG.get("PROJECT_NAME")[0]} B2B API is designed to provide seamless integration for merchants selling a wide range of electronics. Through this API, partnered merchants can manage products, orders, and inventory with ease, while accessing real-time stock levels. +The { + CONSTANCE_CONFIG.get("PROJECT_NAME")[0] # type: ignore [index] + } B2B API is designed to provide seamless integration for merchants selling a wide range of electronics. Through this API, partnered merchants can manage products, orders, and inventory with ease, while accessing real-time stock levels. ## Key Features - **Product Management:** Easily create, update, and manage your product listings with detailed specifications. @@ -67,14 +71,17 @@ The {CONSTANCE_CONFIG.get("PROJECT_NAME")[0]} B2B API is designed to provide sea ## Version Current API version: {EVIBES_VERSION} -""") # noqa: E501, F405 +""" +) # noqa: E501, F405 -# type: ignore -# noinspection Mypy SPECTACULAR_PLATFORM_DESCRIPTION = _(f""" -Welcome to the {CONSTANCE_CONFIG.get("PROJECT_NAME")[0]} Platform API documentation. +Welcome to the { + CONSTANCE_CONFIG.get("PROJECT_NAME")[0] # type: ignore [index] +} Platform API documentation. -The {CONSTANCE_CONFIG.get("PROJECT_NAME")[0]} API is the central hub for managing product listings, monitoring orders, and accessing analytics for your electronics store. It provides RESTful endpoints for managing your store’s backend operations and includes both REST and GraphQL options. +The { + CONSTANCE_CONFIG.get("PROJECT_NAME")[0] # type: ignore [index] +} API is the central hub for managing product listings, monitoring orders, and accessing analytics for your electronics store. It provides RESTful endpoints for managing your store’s backend operations and includes both REST and GraphQL options. ## Key Features - **Product Catalog:** Manage product details, pricing, and availability. @@ -86,8 +93,12 @@ The {CONSTANCE_CONFIG.get("PROJECT_NAME")[0]} API is the central hub for managin ## Authentication - Authentication is handled via JWT tokens. Include the token in the `X-EVIBES-AUTH` header of your requests in the format `Bearer `. -- Access token lifetime is {SIMPLE_JWT.get("ACCESS_TOKEN_LIFETIME").total_seconds() // 60 if not DEBUG else 3600} {"minutes" if not DEBUG else "hours"}. -- Refresh token lifetime is {SIMPLE_JWT.get("ACCESS_TOKEN_LIFETIME").total_seconds() // 3600} hours. +- Access token lifetime is { + SIMPLE_JWT.get("ACCESS_TOKEN_LIFETIME").total_seconds() // 60 if not DEBUG else 3600 # type: ignore [union-attr] +} {"minutes" if not DEBUG else "hours"}. +- Refresh token lifetime is { + SIMPLE_JWT.get("ACCESS_TOKEN_LIFETIME").total_seconds() // 3600 # type: ignore [union-attr] +} hours. - Refresh tokens are automatically invalidated after usage. ## I18N @@ -98,7 +109,7 @@ Current API version: {EVIBES_VERSION} """) # noqa: E501, F405 SPECTACULAR_PLATFORM_SETTINGS = { - "TITLE": f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]} API", + "TITLE": f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]} API", # type: ignore [index] "DESCRIPTION": SPECTACULAR_PLATFORM_DESCRIPTION, "VERSION": EVIBES_VERSION, # noqa: F405 "TOS": "https://wiseless.xyz/evibes/terms-of-service", @@ -136,7 +147,7 @@ SPECTACULAR_PLATFORM_SETTINGS = { }, "SERVERS": [ { - "url": f"https://api.{CONSTANCE_CONFIG.get('BASE_DOMAIN')[0]}/", + "url": f"https://api.{CONSTANCE_CONFIG.get('BASE_DOMAIN')[0]}/", # type: ignore [index] "description": "Production Server", }, {"url": "http://api.localhost:8000/", "description": "Development Server"}, @@ -150,7 +161,7 @@ SPECTACULAR_PLATFORM_SETTINGS = { # noinspection Mypy SPECTACULAR_B2B_SETTINGS = { - "TITLE": f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]} API", + "TITLE": f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]} API", # type: ignore [index] "DESCRIPTION": SPECTACULAR_B2B_DESCRIPTION, "VERSION": EVIBES_VERSION, # noqa: F405 "TOS": "https://wiseless.xyz/evibes/terms-of-service", @@ -188,17 +199,17 @@ SPECTACULAR_B2B_SETTINGS = { }, "SERVERS": [ { - "url": f"https://b2b.{CONSTANCE_CONFIG.get('BASE_DOMAIN')[0]}/", + "url": f"https://b2b.{CONSTANCE_CONFIG.get('BASE_DOMAIN')[0]}/", # type: ignore [index] "description": "Production Server", }, { - "url": f"https://beta.b2b.{CONSTANCE_CONFIG.get('BASE_DOMAIN')[0]}/", + "url": f"https://beta.b2b.{CONSTANCE_CONFIG.get('BASE_DOMAIN')[0]}/", # type: ignore [index] "description": "Beta Solutions Server", }, ], "CONTACT": { - "name": f"{CONSTANCE_CONFIG.get('COMPANY_NAME')[0]}", - "email": f"{CONSTANCE_CONFIG.get('EMAIL_HOST_USER')[0]}", - "URL": f"https://www.{CONSTANCE_CONFIG.get('BASE_DOMAIN')[0]}/help", + "name": f"{CONSTANCE_CONFIG.get('COMPANY_NAME')[0]}", # type: ignore [index] + "email": f"{CONSTANCE_CONFIG.get('EMAIL_HOST_USER')[0]}", # type: ignore [index] + "URL": f"https://www.{CONSTANCE_CONFIG.get('BASE_DOMAIN')[0]}/help", # type: ignore [index] }, } diff --git a/evibes/settings/extensions.py b/evibes/settings/extensions.py index 9d3cff60..eed9c571 100644 --- a/evibes/settings/extensions.py +++ b/evibes/settings/extensions.py @@ -1,3 +1,4 @@ +# noinspection PyUnresolvedReferences from evibes.settings.base import * # noqa: F403 GRAPH_MODELS = { diff --git a/vibes_auth/graphene/object_types.py b/vibes_auth/graphene/object_types.py index a91ca2b3..71c32035 100644 --- a/vibes_auth/graphene/object_types.py +++ b/vibes_auth/graphene/object_types.py @@ -5,7 +5,7 @@ from graphene.types.generic import GenericScalar from graphene_django import DjangoObjectType from graphql_relay.connection.array_connection import connection_from_array -from core.graphene.object_types import OrderType, ProductType, WishlistType +from core.graphene.object_types import OrderType, ProductType, WishlistType, AddressType from core.models import Product, Wishlist from evibes.settings import LANGUAGE_CODE, LANGUAGES from payments.graphene.object_types import BalanceType @@ -47,6 +47,7 @@ class UserType(DjangoObjectType): avatar = String(description=_("avatar")) attributes = GenericScalar(description=_("attributes may be used to store custom data")) language = String(description=_(f"language is one of the {LANGUAGES} with default {LANGUAGE_CODE}")) + addresses = Field(lambda: AddressType, source="address_set", description=_("address set")) class Meta: model = User diff --git a/vibes_auth/views.py b/vibes_auth/views.py index 3ef1b658..39326f5d 100644 --- a/vibes_auth/views.py +++ b/vibes_auth/views.py @@ -25,10 +25,10 @@ logger = logging.getLogger("django") @extend_schema_view(**TOKEN_OBTAIN_SCHEMA) class TokenObtainPairView(TokenViewBase): """ - Represents a view for obtaining a pair of access and refresh tokens. + Represents a view for getting a pair of access and refresh tokens. This view manages the process of handling token-based authentication where - clients can obtain a pair of JWT tokens (access and refresh) using provided + clients can get a pair of JWT tokens (access and refresh) using provided credentials. It is built on top of a base token view and ensures proper rate limiting to protect against brute force attacks. @@ -39,7 +39,7 @@ class TokenObtainPairView(TokenViewBase): Usage: This view should be used in authentication-related APIs where clients - need to obtain new sets of tokens. It incorporates both a serializer for + need to get new sets of tokens. It incorporates both a serializer for processing incoming data and also rate limiting to enforce request limits. Methods: diff --git a/vibes_auth/viewsets.py b/vibes_auth/viewsets.py index e4f048b1..18373a36 100644 --- a/vibes_auth/viewsets.py +++ b/vibes_auth/viewsets.py @@ -39,7 +39,7 @@ class UserViewSet( GenericViewSet, ): """ - User view set implementation using Django REST framework. + User view set implementation. Provides a set of actions that manage user-related data such as creation, retrieval, updates, deletion, and custom actions including password reset,