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:
parent
661f9f2fe6
commit
09366213b6
10 changed files with 62 additions and 42 deletions
|
|
@ -38,7 +38,6 @@ __pypackages__/
|
|||
# Packaging and distribution
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
build/
|
||||
dist/
|
||||
dist-ssr/
|
||||
*.egg
|
||||
*.egg-info/
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
6
core/vendors/__init__.py
vendored
6
core/vendors/__init__.py
vendored
|
|
@ -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...")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 <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"}.
|
||||
- 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]
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# noinspection PyUnresolvedReferences
|
||||
from evibes.settings.base import * # noqa: F403
|
||||
|
||||
GRAPH_MODELS = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue