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`.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-07-12 18:49:25 +03:00
parent 661f9f2fe6
commit 09366213b6
10 changed files with 62 additions and 42 deletions

View file

@ -38,7 +38,6 @@ __pypackages__/
# Packaging and distribution # Packaging and distribution
# ────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────
build/ build/
dist/
dist-ssr/ dist-ssr/
*.egg *.egg
*.egg-info/ *.egg-info/

View file

@ -1,6 +1,7 @@
import logging import logging
from django.core.cache import cache from django.core.cache import cache
from django.utils import timezone
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from graphene import Field, List, ObjectType, Schema from graphene import Field, List, ObjectType, Schema
from graphene_django.filter import DjangoFilterConnectionField from graphene_django.filter import DjangoFilterConnectionField
@ -14,6 +15,7 @@ from core.filters import (
OrderFilter, OrderFilter,
ProductFilter, ProductFilter,
WishlistFilter, WishlistFilter,
AddressFilter,
) )
from core.graphene.mutations import ( from core.graphene.mutations import (
AddOrderProduct, AddOrderProduct,
@ -58,6 +60,7 @@ from core.graphene.object_types import (
StockType, StockType,
VendorType, VendorType,
WishlistType, WishlistType,
AddressType,
) )
from core.models import ( from core.models import (
AttributeGroup, AttributeGroup,
@ -75,6 +78,7 @@ from core.models import (
Stock, Stock,
Vendor, Vendor,
Wishlist, Wishlist,
Address,
) )
from core.utils import get_project_parameters from core.utils import get_project_parameters
from core.utils.languages import get_flag_by_language from core.utils.languages import get_flag_by_language
@ -106,6 +110,7 @@ class Query(ObjectType):
products = DjangoFilterConnectionField(ProductType, filterset_class=ProductFilter) products = DjangoFilterConnectionField(ProductType, filterset_class=ProductFilter)
orders = DjangoFilterConnectionField(OrderType, filterset_class=OrderFilter) orders = DjangoFilterConnectionField(OrderType, filterset_class=OrderFilter)
users = DjangoFilterConnectionField(UserType, filterset_class=UserFilter) users = DjangoFilterConnectionField(UserType, filterset_class=UserFilter)
addresses = DjangoFilterConnectionField(AddressType, filterset_class=AddressFilter)
attribute_groups = DjangoFilterConnectionField(AttributeGroupType) attribute_groups = DjangoFilterConnectionField(AttributeGroupType)
categories = DjangoFilterConnectionField(CategoryType, filterset_class=CategoryFilter) categories = DjangoFilterConnectionField(CategoryType, filterset_class=CategoryFilter)
vendors = DjangoFilterConnectionField(VendorType) vendors = DjangoFilterConnectionField(VendorType)
@ -294,7 +299,7 @@ class Query(ObjectType):
is_active=True, is_active=True,
user=info.context.user, user=info.context.user,
used_on__isnull=True, used_on__isnull=True,
start_time__lte=info.context.now, start_time__lte=timezone.now(),
) )
@staticmethod @staticmethod
@ -309,6 +314,12 @@ class Query(ObjectType):
return CategoryTag.objects.all() return CategoryTag.objects.all()
return CategoryTag.objects.filter(is_active=True) 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): class Mutation(ObjectType):
search = Search.Field() search = Search.Field()

View file

@ -1,7 +1,7 @@
import datetime import datetime
import json import json
import logging import logging
from typing import Optional, Self, Any from typing import Any, Optional, Self
from constance import config from constance import config
from django.contrib.gis.db.models import PointField from django.contrib.gis.db.models import PointField
@ -28,8 +28,8 @@ from django.db.models import (
Max, Max,
OneToOneField, OneToOneField,
PositiveIntegerField, PositiveIntegerField,
TextField,
QuerySet, QuerySet,
TextField,
) )
from django.db.models.indexes import Index from django.db.models.indexes import Index
from django.http import Http404 from django.http import Http404
@ -1536,12 +1536,11 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
raise Http404(_("promocode does not exist")) from dne raise Http404(_("promocode does not exist")) from dne
return promocode.use(self) 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: try:
if not any([shipping_address_uuid, billing_address_uuid]): if not any([shipping_address_uuid, billing_address_uuid]) and not self.is_whole_digital:
if self.is_whole_digital:
return
else:
raise ValueError(_("you can only buy physical products with shipping address specified")) raise ValueError(_("you can only buy physical products with shipping address specified"))
if billing_address_uuid and not shipping_address_uuid: if billing_address_uuid and not shipping_address_uuid:
@ -1553,8 +1552,8 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
shipping_address = billing_address shipping_address = billing_address
else: else:
billing_address = Address.objects.get(uuid=billing_address_uuid) billing_address = Address.objects.get(uuid=billing_address_uuid) # type: ignore [misc]
shipping_address = Address.objects.get(uuid=shipping_address_uuid) shipping_address = Address.objects.get(uuid=shipping_address_uuid) # type: ignore [misc]
self.billing_address = billing_address self.billing_address = billing_address
self.shipping_address = shipping_address self.shipping_address = shipping_address

View file

@ -301,13 +301,11 @@ class AbstractVendor:
raise ValueError(f"Invalid method {method!r} for products update...") raise ValueError(f"Invalid method {method!r} for products update...")
def delete_inactives(self, inactivation_method: str = "deactivate", size: int = 5000): def delete_inactives(self, inactivation_method: str = "deactivate", size: int = 5000):
filter_kwargs: dict[str, Any] = {}
# noinspection PyUnreachableCode
match inactivation_method: match inactivation_method:
case "deactivate": case "deactivate":
filter_kwargs = {"is_active": False} filter_kwargs: dict[str, Any] = {"is_active": False}
case "description": case "description":
filter_kwargs = {"description__exact": "EVIBES_DELETED_PRODUCT"} filter_kwargs: dict[str, Any] = {"description__exact": "EVIBES_DELETED_PRODUCT"}
case _: case _:
raise ValueError(f"Invalid method {inactivation_method!r} for products cleaner...") raise ValueError(f"Invalid method {inactivation_method!r} for products cleaner...")

View file

@ -421,7 +421,7 @@ class BuyAsBusinessView(APIView):
Methods: Methods:
post(request, *_args, **kwargs): 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) @ratelimit(key="ip", rate="2/h", block=True)

View file

@ -45,11 +45,15 @@ SIMPLE_JWT: dict[str, timedelta | str | bool] = {
"AUTH_HEADER_NAME": "HTTP_X_EVIBES_AUTH", "AUTH_HEADER_NAME": "HTTP_X_EVIBES_AUTH",
} }
# type: ignore SPECTACULAR_B2B_DESCRIPTION = _( # type: ignore [index]
SPECTACULAR_B2B_DESCRIPTION = _(f""" f"""
Welcome to the {CONSTANCE_CONFIG.get("PROJECT_NAME")[0]} B2B API documentation. 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 ## Key Features
- **Product Management:** Easily create, update, and manage your product listings with detailed specifications. - **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 ## Version
Current API version: {EVIBES_VERSION} Current API version: {EVIBES_VERSION}
""") # noqa: E501, F405 """
) # noqa: E501, F405
# type: ignore
# noinspection Mypy
SPECTACULAR_PLATFORM_DESCRIPTION = _(f""" 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 stores 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 stores backend operations and includes both REST and GraphQL options.
## Key Features ## Key Features
- **Product Catalog:** Manage product details, pricing, and availability. - **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
- Authentication is handled via JWT tokens. Include the token in the `X-EVIBES-AUTH` header of your requests in the format `Bearer <your_token>`. - Authentication is handled via JWT tokens. Include the token in the `X-EVIBES-AUTH` header of your requests in the format `Bearer <your_token>`.
- Access token lifetime is {SIMPLE_JWT.get("ACCESS_TOKEN_LIFETIME").total_seconds() // 60 if not DEBUG else 3600} {"minutes" if not DEBUG else "hours"}. - Access token lifetime is {
- Refresh token lifetime is {SIMPLE_JWT.get("ACCESS_TOKEN_LIFETIME").total_seconds() // 3600} hours. 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. - Refresh tokens are automatically invalidated after usage.
## I18N ## I18N
@ -98,7 +109,7 @@ Current API version: {EVIBES_VERSION}
""") # noqa: E501, F405 """) # noqa: E501, F405
SPECTACULAR_PLATFORM_SETTINGS = { 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, "DESCRIPTION": SPECTACULAR_PLATFORM_DESCRIPTION,
"VERSION": EVIBES_VERSION, # noqa: F405 "VERSION": EVIBES_VERSION, # noqa: F405
"TOS": "https://wiseless.xyz/evibes/terms-of-service", "TOS": "https://wiseless.xyz/evibes/terms-of-service",
@ -136,7 +147,7 @@ SPECTACULAR_PLATFORM_SETTINGS = {
}, },
"SERVERS": [ "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", "description": "Production Server",
}, },
{"url": "http://api.localhost:8000/", "description": "Development Server"}, {"url": "http://api.localhost:8000/", "description": "Development Server"},
@ -150,7 +161,7 @@ SPECTACULAR_PLATFORM_SETTINGS = {
# noinspection Mypy # noinspection Mypy
SPECTACULAR_B2B_SETTINGS = { 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, "DESCRIPTION": SPECTACULAR_B2B_DESCRIPTION,
"VERSION": EVIBES_VERSION, # noqa: F405 "VERSION": EVIBES_VERSION, # noqa: F405
"TOS": "https://wiseless.xyz/evibes/terms-of-service", "TOS": "https://wiseless.xyz/evibes/terms-of-service",
@ -188,17 +199,17 @@ SPECTACULAR_B2B_SETTINGS = {
}, },
"SERVERS": [ "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", "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", "description": "Beta Solutions Server",
}, },
], ],
"CONTACT": { "CONTACT": {
"name": f"{CONSTANCE_CONFIG.get('COMPANY_NAME')[0]}", "name": f"{CONSTANCE_CONFIG.get('COMPANY_NAME')[0]}", # type: ignore [index]
"email": f"{CONSTANCE_CONFIG.get('EMAIL_HOST_USER')[0]}", "email": f"{CONSTANCE_CONFIG.get('EMAIL_HOST_USER')[0]}", # type: ignore [index]
"URL": f"https://www.{CONSTANCE_CONFIG.get('BASE_DOMAIN')[0]}/help", "URL": f"https://www.{CONSTANCE_CONFIG.get('BASE_DOMAIN')[0]}/help", # type: ignore [index]
}, },
} }

View file

@ -1,3 +1,4 @@
# noinspection PyUnresolvedReferences
from evibes.settings.base import * # noqa: F403 from evibes.settings.base import * # noqa: F403
GRAPH_MODELS = { GRAPH_MODELS = {

View file

@ -5,7 +5,7 @@ from graphene.types.generic import GenericScalar
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphql_relay.connection.array_connection import connection_from_array 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 core.models import Product, Wishlist
from evibes.settings import LANGUAGE_CODE, LANGUAGES from evibes.settings import LANGUAGE_CODE, LANGUAGES
from payments.graphene.object_types import BalanceType from payments.graphene.object_types import BalanceType
@ -47,6 +47,7 @@ class UserType(DjangoObjectType):
avatar = String(description=_("avatar")) avatar = String(description=_("avatar"))
attributes = GenericScalar(description=_("attributes may be used to store custom data")) 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}")) 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: class Meta:
model = User model = User

View file

@ -25,10 +25,10 @@ logger = logging.getLogger("django")
@extend_schema_view(**TOKEN_OBTAIN_SCHEMA) @extend_schema_view(**TOKEN_OBTAIN_SCHEMA)
class TokenObtainPairView(TokenViewBase): 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 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 credentials. It is built on top of a base token view and ensures proper
rate limiting to protect against brute force attacks. rate limiting to protect against brute force attacks.
@ -39,7 +39,7 @@ class TokenObtainPairView(TokenViewBase):
Usage: Usage:
This view should be used in authentication-related APIs where clients 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. processing incoming data and also rate limiting to enforce request limits.
Methods: Methods:

View file

@ -39,7 +39,7 @@ class UserViewSet(
GenericViewSet, 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, Provides a set of actions that manage user-related data such as creation,
retrieval, updates, deletion, and custom actions including password reset, retrieval, updates, deletion, and custom actions including password reset,