Features: 1) Introduced strict parameter for zip function in widgets.py; 2) Added EXTENSIONS_MAX_UNIQUE_QUERY_ATTEMPTS setting;

Fixes: 1) Resolved redundant lines and formatting inconsistencies across multiple files; 2) Corrected Collection typing imports and Optional replacements with union types (e.g., `str | None`);

Extra: Improved formatting and readability by consolidating single-line code sections and simplifying expressions.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-06-21 20:38:37 +03:00
parent 67794a7997
commit fdd92dbf8b
24 changed files with 193 additions and 447 deletions

View file

@ -424,9 +424,7 @@ class ConstanceAdmin(BaseConstanceAdmin):
self.admin_site.admin_view(self.changelist_view), self.admin_site.admin_view(self.changelist_view),
name=f"{info}_changelist", name=f"{info}_changelist",
), ),
path( path("", self.admin_site.admin_view(self.changelist_view), name=f"{info}_add"),
"", self.admin_site.admin_view(self.changelist_view), name=f"{info}_add"
),
] ]

View file

@ -33,9 +33,7 @@ SMART_FIELDS = [
] ]
def process_query( def process_query(query: str = "", request: Request | None = None) -> dict[str, list[dict]] | None:
query: str = "", request: Request | None = None
) -> dict[str, list[dict]] | None:
""" """
Perform a lenient, typotolerant, multiindex search. Perform a lenient, typotolerant, multiindex search.
@ -84,19 +82,13 @@ def process_query(
boost_mode="sum", boost_mode="sum",
) )
search = ( search = Search(index=["products", "categories", "brands", "posts"]).query(function_score_query).extra(size=100)
Search(index=["products", "categories", "brands", "posts"])
.query(function_score_query)
.extra(size=100)
)
response = search.execute() response = search.execute()
results: dict = {"products": [], "categories": [], "brands": [], "posts": []} results: dict = {"products": [], "categories": [], "brands": [], "posts": []}
for hit in response.hits: for hit in response.hits:
obj_uuid = getattr(hit, "uuid", None) or hit.meta.id obj_uuid = getattr(hit, "uuid", None) or hit.meta.id
obj_name = ( obj_name = getattr(hit, "name", None) or getattr(hit, "title", None) or "N/A"
getattr(hit, "name", None) or getattr(hit, "title", None) or "N/A"
)
obj_slug = "" obj_slug = ""
raw_slug = getattr(hit, "slug", None) raw_slug = getattr(hit, "slug", None)
if raw_slug: if raw_slug:
@ -226,9 +218,7 @@ def _add_multilang_fields(cls):
copy_to="name", copy_to="name",
fields={ fields={
"raw": fields.KeywordField(ignore_above=256), "raw": fields.KeywordField(ignore_above=256),
"ngram": fields.TextField( "ngram": fields.TextField(analyzer="name_ngram", search_analyzer="query_lc"),
analyzer="name_ngram", search_analyzer="query_lc"
),
"phonetic": fields.TextField(analyzer="name_phonetic"), "phonetic": fields.TextField(analyzer="name_phonetic"),
}, },
), ),
@ -251,9 +241,7 @@ def _add_multilang_fields(cls):
copy_to="description", copy_to="description",
fields={ fields={
"raw": fields.KeywordField(ignore_above=256), "raw": fields.KeywordField(ignore_above=256),
"ngram": fields.TextField( "ngram": fields.TextField(analyzer="name_ngram", search_analyzer="query_lc"),
analyzer="name_ngram", search_analyzer="query_lc"
),
"phonetic": fields.TextField(analyzer="name_phonetic"), "phonetic": fields.TextField(analyzer="name_phonetic"),
}, },
), ),

View file

@ -11,13 +11,9 @@ class _BaseDoc(ActiveOnlyMixin, Document):
analyzer="standard", analyzer="standard",
fields={ fields={
"raw": fields.KeywordField(ignore_above=256), "raw": fields.KeywordField(ignore_above=256),
"ngram": fields.TextField( "ngram": fields.TextField(analyzer="name_ngram", search_analyzer="query_lc"),
analyzer="name_ngram", search_analyzer="query_lc"
),
"phonetic": fields.TextField(analyzer="name_phonetic"), "phonetic": fields.TextField(analyzer="name_phonetic"),
"auto": fields.TextField( "auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
analyzer="autocomplete", search_analyzer="autocomplete_search"
),
}, },
) )
description = fields.TextField( description = fields.TextField(
@ -25,13 +21,9 @@ class _BaseDoc(ActiveOnlyMixin, Document):
analyzer="standard", analyzer="standard",
fields={ fields={
"raw": fields.KeywordField(ignore_above=256), "raw": fields.KeywordField(ignore_above=256),
"ngram": fields.TextField( "ngram": fields.TextField(analyzer="name_ngram", search_analyzer="query_lc"),
analyzer="name_ngram", search_analyzer="query_lc"
),
"phonetic": fields.TextField(analyzer="name_phonetic"), "phonetic": fields.TextField(analyzer="name_phonetic"),
"auto": fields.TextField( "auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
analyzer="autocomplete", search_analyzer="autocomplete_search"
),
}, },
) )
slug = fields.KeywordField(attr="slug", index=False) slug = fields.KeywordField(attr="slug", index=False)
@ -90,9 +82,7 @@ class BrandDocument(ActiveOnlyMixin, Document):
analyzer="standard", analyzer="standard",
fields={ fields={
"raw": fields.KeywordField(ignore_above=256), "raw": fields.KeywordField(ignore_above=256),
"ngram": fields.TextField( "ngram": fields.TextField(analyzer="name_ngram", search_analyzer="query_lc"),
analyzer="name_ngram", search_analyzer="query_lc"
),
"phonetic": fields.TextField(analyzer="name_phonetic"), "phonetic": fields.TextField(analyzer="name_phonetic"),
}, },
) )

View file

@ -60,31 +60,19 @@ class CaseInsensitiveListFilter(BaseInFilter, CharFilter):
class ProductFilter(FilterSet): class ProductFilter(FilterSet):
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID")) uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID"))
name = CharFilter(method="filter_name", label=_("Name")) name = CharFilter(method="filter_name", label=_("Name"))
categories = CaseInsensitiveListFilter( categories = CaseInsensitiveListFilter(field_name="category__name", label=_("Categories"))
field_name="category__name", label=_("Categories")
)
category_uuid = CharFilter(method="filter_category", label="Category (UUID)") category_uuid = CharFilter(method="filter_category", label="Category (UUID)")
categories_slugs = CaseInsensitiveListFilter( categories_slugs = CaseInsensitiveListFilter(field_name="category__slug", label=_("Categories Slugs"))
field_name="category__slug", label=_("Categories Slugs")
)
tags = CaseInsensitiveListFilter(field_name="tags__tag_name", label=_("Tags")) tags = CaseInsensitiveListFilter(field_name="tags__tag_name", label=_("Tags"))
min_price = NumberFilter( min_price = NumberFilter(field_name="stocks__price", lookup_expr="gte", label=_("Min Price"))
field_name="stocks__price", lookup_expr="gte", label=_("Min Price") max_price = NumberFilter(field_name="stocks__price", lookup_expr="lte", label=_("Max Price"))
)
max_price = NumberFilter(
field_name="stocks__price", lookup_expr="lte", label=_("Max Price")
)
is_active = BooleanFilter(field_name="is_active", label=_("Is Active")) is_active = BooleanFilter(field_name="is_active", label=_("Is Active"))
brand = CharFilter(field_name="brand__name", lookup_expr="iexact", label=_("Brand")) brand = CharFilter(field_name="brand__name", lookup_expr="iexact", label=_("Brand"))
attributes = CharFilter(method="filter_attributes", label=_("Attributes")) attributes = CharFilter(method="filter_attributes", label=_("Attributes"))
quantity = NumberFilter( quantity = NumberFilter(field_name="stocks__quantity", lookup_expr="gt", label=_("Quantity"))
field_name="stocks__quantity", lookup_expr="gt", label=_("Quantity")
)
slug = CharFilter(field_name="slug", lookup_expr="exact", label=_("Slug")) slug = CharFilter(field_name="slug", lookup_expr="exact", label=_("Slug"))
is_digital = BooleanFilter(field_name="is_digital", label=_("Is Digital")) is_digital = BooleanFilter(field_name="is_digital", label=_("Is Digital"))
include_subcategories = BooleanFilter( include_subcategories = BooleanFilter(method="filter_include_flag", label=_("Include sub-categories"))
method="filter_include_flag", label=_("Include sub-categories")
)
order_by = OrderingFilter( order_by = OrderingFilter(
fields=( fields=(
@ -152,15 +140,11 @@ class ProductFilter(FilterSet):
output_field=IntegerField(), output_field=IntegerField(),
) )
return ( return queryset.filter(uuid__in=uuids).annotate(_order=ordering).order_by("_order")
queryset.filter(uuid__in=uuids).annotate(_order=ordering).order_by("_order")
)
def filter_include_flag(self, queryset, **_kwargs): def filter_include_flag(self, queryset, **_kwargs):
if not self.data.get("category_uuid"): if not self.data.get("category_uuid"):
raise BadRequest( raise BadRequest(_("there must be a category_uuid to use include_subcategories flag"))
_("there must be a category_uuid to use include_subcategories flag")
)
return queryset return queryset
def filter_attributes(self, queryset, _name, value): def filter_attributes(self, queryset, _name, value):
@ -247,7 +231,7 @@ class ProductFilter(FilterSet):
def _infer_type(value): def _infer_type(value):
try: try:
parsed_value = json.loads(value) parsed_value = json.loads(value)
if isinstance(parsed_value, (list, dict)): if isinstance(parsed_value, list | dict):
return parsed_value return parsed_value
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
pass pass
@ -296,20 +280,12 @@ class OrderFilter(FilterSet):
label=_("Search (ID, product name or part number)"), label=_("Search (ID, product name or part number)"),
) )
min_buy_time = DateTimeFilter( min_buy_time = DateTimeFilter(field_name="buy_time", lookup_expr="gte", label=_("Bought after (inclusive)"))
field_name="buy_time", lookup_expr="gte", label=_("Bought after (inclusive)") max_buy_time = DateTimeFilter(field_name="buy_time", lookup_expr="lte", label=_("Bought before (inclusive)"))
)
max_buy_time = DateTimeFilter(
field_name="buy_time", lookup_expr="lte", label=_("Bought before (inclusive)")
)
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact") uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
user_email = CharFilter( user_email = CharFilter(field_name="user__email", lookup_expr="iexact", label=_("User email"))
field_name="user__email", lookup_expr="iexact", label=_("User email") user = UUIDFilter(field_name="user__uuid", lookup_expr="exact", label=_("User UUID"))
)
user = UUIDFilter(
field_name="user__uuid", lookup_expr="exact", label=_("User UUID")
)
status = CharFilter(field_name="status", lookup_expr="icontains", label=_("Status")) status = CharFilter(field_name="status", lookup_expr="icontains", label=_("Status"))
human_readable_id = CharFilter( human_readable_id = CharFilter(
field_name="human_readable_id", field_name="human_readable_id",
@ -355,12 +331,8 @@ class OrderFilter(FilterSet):
class WishlistFilter(FilterSet): class WishlistFilter(FilterSet):
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact") uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
user_email = CharFilter( user_email = CharFilter(field_name="user__email", lookup_expr="iexact", label=_("User email"))
field_name="user__email", lookup_expr="iexact", label=_("User email") user = UUIDFilter(field_name="user__uuid", lookup_expr="exact", label=_("User UUID"))
)
user = UUIDFilter(
field_name="user__uuid", lookup_expr="exact", label=_("User UUID")
)
order_by = OrderingFilter( order_by = OrderingFilter(
fields=( fields=(
@ -422,9 +394,7 @@ class CategoryFilter(FilterSet):
output_field=IntegerField(), output_field=IntegerField(),
) )
return ( return queryset.filter(uuid__in=uuids).annotate(_order=ordering).order_by("_order")
queryset.filter(uuid__in=uuids).annotate(_order=ordering).order_by("_order")
)
def filter_whole_categories(self, queryset, _name, value): def filter_whole_categories(self, queryset, _name, value):
has_own_products = Exists(Product.objects.filter(category=OuterRef("pk"))) has_own_products = Exists(Product.objects.filter(category=OuterRef("pk")))
@ -456,9 +426,7 @@ class CategoryFilter(FilterSet):
class BrandFilter(FilterSet): class BrandFilter(FilterSet):
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact") uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
name = CharFilter(method="filter_name", label=_("Name")) name = CharFilter(method="filter_name", label=_("Name"))
categories = CaseInsensitiveListFilter( categories = CaseInsensitiveListFilter(field_name="categories__uuid", lookup_expr="exact", label=_("Categories"))
field_name="categories__uuid", lookup_expr="exact", label=_("Categories")
)
order_by = OrderingFilter( order_by = OrderingFilter(
fields=( fields=(
@ -484,9 +452,7 @@ class BrandFilter(FilterSet):
output_field=IntegerField(), output_field=IntegerField(),
) )
return ( return queryset.filter(uuid__in=uuids).annotate(_order=ordering).order_by("_order")
queryset.filter(uuid__in=uuids).annotate(_order=ordering).order_by("_order")
)
class FeedbackFilter(FilterSet): class FeedbackFilter(FilterSet):
@ -520,12 +486,8 @@ class FeedbackFilter(FilterSet):
class AddressFilter(FilterSet): class AddressFilter(FilterSet):
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID")) uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID"))
user_uuid = UUIDFilter( user_uuid = UUIDFilter(field_name="user__uuid", lookup_expr="exact", label=_("User UUID"))
field_name="user__uuid", lookup_expr="exact", label=_("User UUID") user_email = CharFilter(field_name="user__email", lookup_expr="iexact", label=_("User email"))
)
user_email = CharFilter(
field_name="user__email", lookup_expr="iexact", label=_("User email")
)
order_by = OrderingFilter( order_by = OrderingFilter(
fields=( fields=(

View file

@ -35,9 +35,7 @@ class CacheOperator(BaseMutation):
description = _("cache I/O") description = _("cache I/O")
class Arguments: class Arguments:
key = String( key = String(required=True, description=_("key to look for in or set into the cache"))
required=True, description=_("key to look for in or set into the cache")
)
data = GenericScalar(required=False, description=_("data to store in cache")) data = GenericScalar(required=False, description=_("data to store in cache"))
timeout = Int( timeout = Int(
required=False, required=False,
@ -67,9 +65,7 @@ class RequestCursedURL(BaseMutation):
try: try:
data = cache.get(url, None) data = cache.get(url, None)
if not data: if not data:
response = requests.get( response = requests.get(url, headers={"content-type": "application/json"})
url, headers={"content-type": "application/json"}
)
response.raise_for_status() response.raise_for_status()
data = camelize(response.json()) data = camelize(response.json())
cache.set(url, data, 86400) cache.set(url, data, 86400)
@ -97,9 +93,7 @@ class AddOrderProduct(BaseMutation):
if not (user.has_perm("core.add_orderproduct") or user == order.user): if not (user.has_perm("core.add_orderproduct") or user == order.user):
raise PermissionDenied(permission_denied_message) raise PermissionDenied(permission_denied_message)
order = order.add_product( order = order.add_product(product_uuid=product_uuid, attributes=format_attributes(attributes))
product_uuid=product_uuid, attributes=format_attributes(attributes)
)
return AddOrderProduct(order=order) return AddOrderProduct(order=order)
except Order.DoesNotExist: except Order.DoesNotExist:
@ -125,9 +119,7 @@ class RemoveOrderProduct(BaseMutation):
if not (user.has_perm("core.change_orderproduct") or user == order.user): if not (user.has_perm("core.change_orderproduct") or user == order.user):
raise PermissionDenied(permission_denied_message) raise PermissionDenied(permission_denied_message)
order = order.remove_product( order = order.remove_product(product_uuid=product_uuid, attributes=format_attributes(attributes))
product_uuid=product_uuid, attributes=format_attributes(attributes)
)
return AddOrderProduct(order=order) return AddOrderProduct(order=order)
except Order.DoesNotExist: except Order.DoesNotExist:
@ -206,11 +198,7 @@ class BuyOrder(BaseMutation):
billing_address=None, billing_address=None,
): ):
if not any([order_uuid, order_hr_id]) or all([order_uuid, order_hr_id]): if not any([order_uuid, order_hr_id]) or all([order_uuid, order_hr_id]):
raise BadRequest( raise BadRequest(_("please provide either order_uuid or order_hr_id - mutually exclusive"))
_(
"please provide either order_uuid or order_hr_id - mutually exclusive"
)
)
user = info.context.user user = info.context.user
try: try:
order = None order = None
@ -234,11 +222,7 @@ class BuyOrder(BaseMutation):
case "<class 'core.models.Order'>": case "<class 'core.models.Order'>":
return BuyOrder(order=instance) return BuyOrder(order=instance)
case _: case _:
raise TypeError( raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}"))
_(
f"wrong type came from order.buy() method: {type(instance)!s}"
)
)
except Order.DoesNotExist: except Order.DoesNotExist:
raise Http404(_(f"order {order_uuid} not found")) raise Http404(_(f"order {order_uuid} not found"))
@ -266,11 +250,7 @@ class BulkOrderAction(BaseMutation):
order_hr_id=None, order_hr_id=None,
): ):
if not any([order_uuid, order_hr_id]) or all([order_uuid, order_hr_id]): if not any([order_uuid, order_hr_id]) or all([order_uuid, order_hr_id]):
raise BadRequest( raise BadRequest(_("please provide either order_uuid or order_hr_id - mutually exclusive"))
_(
"please provide either order_uuid or order_hr_id - mutually exclusive"
)
)
user = info.context.user user = info.context.user
try: try:
order = None order = None
@ -452,20 +432,14 @@ class BuyWishlist(BaseMutation):
): ):
order.add_product(product_uuid=product.pk) order.add_product(product_uuid=product.pk)
instance = order.buy( instance = order.buy(force_balance=force_balance, force_payment=force_payment)
force_balance=force_balance, force_payment=force_payment
)
match str(type(instance)): match str(type(instance)):
case "<class 'payments.models.Transaction'>": case "<class 'payments.models.Transaction'>":
return BuyWishlist(transaction=instance) return BuyWishlist(transaction=instance)
case "<class 'core.models.Order'>": case "<class 'core.models.Order'>":
return BuyWishlist(order=instance) return BuyWishlist(order=instance)
case _: case _:
raise TypeError( raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}"))
_(
f"wrong type came from order.buy() method: {type(instance)!s}"
)
)
except Wishlist.DoesNotExist: except Wishlist.DoesNotExist:
raise Http404(_(f"wishlist {wishlist_uuid} not found")) raise Http404(_(f"wishlist {wishlist_uuid} not found"))
@ -479,9 +453,7 @@ class BuyProduct(BaseMutation):
product_uuid = UUID(required=True) product_uuid = UUID(required=True)
attributes = String( attributes = String(
required=False, required=False,
description=_( description=_("please send the attributes as the string formatted like attr1=value1,attr2=value2"),
"please send the attributes as the string formatted like attr1=value1,attr2=value2"
),
) )
force_balance = Boolean(required=False) force_balance = Boolean(required=False)
force_payment = Boolean(required=False) force_payment = Boolean(required=False)
@ -500,9 +472,7 @@ class BuyProduct(BaseMutation):
): ):
user = info.context.user user = info.context.user
order = Order.objects.create(user=user, status="MOMENTAL") order = Order.objects.create(user=user, status="MOMENTAL")
order.add_product( order.add_product(product_uuid=product_uuid, attributes=format_attributes(attributes))
product_uuid=product_uuid, attributes=format_attributes(attributes)
)
instance = order.buy(force_balance=force_balance, force_payment=force_payment) instance = order.buy(force_balance=force_balance, force_payment=force_payment)
match str(type(instance)): match str(type(instance)):
case "<class 'payments.models.Transaction'>": case "<class 'payments.models.Transaction'>":
@ -510,9 +480,7 @@ class BuyProduct(BaseMutation):
case "<class 'core.models.Order'>": case "<class 'core.models.Order'>":
return BuyProduct(order=instance) return BuyProduct(order=instance)
case _: case _:
raise TypeError( raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}"))
_(f"wrong type came from order.buy() method: {type(instance)!s}")
)
class CreateProduct(BaseMutation): class CreateProduct(BaseMutation):
@ -528,9 +496,7 @@ class CreateProduct(BaseMutation):
if not info.context.user.has_perm("core.add_product"): if not info.context.user.has_perm("core.add_product"):
raise PermissionDenied(permission_denied_message) raise PermissionDenied(permission_denied_message)
category = Category.objects.get(uuid=category_uuid) category = Category.objects.get(uuid=category_uuid)
product = Product.objects.create( product = Product.objects.create(name=name, description=description, category=category)
name=name, description=description, category=category
)
return CreateProduct(product=product) return CreateProduct(product=product)
@ -577,9 +543,7 @@ class DeleteProduct(BaseMutation):
class CreateAddress(BaseMutation): class CreateAddress(BaseMutation):
class Arguments: class Arguments:
raw_data = String( raw_data = String(required=True, description=_("original address string provided by the user"))
required=True, description=_("original address string provided by the user")
)
address = Field(AddressType) address = Field(AddressType)

View file

@ -101,16 +101,10 @@ class BrandType(DjangoObjectType):
return self.categories.filter(is_active=True) return self.categories.filter(is_active=True)
def resolve_big_logo(self: Brand, info): def resolve_big_logo(self: Brand, info):
return ( return info.context.build_absolute_uri(self.big_logo.url) if self.big_logo else ""
info.context.build_absolute_uri(self.big_logo.url) if self.big_logo else ""
)
def resolve_small_logo(self: Brand, info): def resolve_small_logo(self: Brand, info):
return ( return info.context.build_absolute_uri(self.small_logo.url) if self.small_logo else ""
info.context.build_absolute_uri(self.small_logo.url)
if self.small_logo
else ""
)
class FilterableAttributeType(ObjectType): class FilterableAttributeType(ObjectType):
@ -132,22 +126,14 @@ class CategoryType(DjangoObjectType):
markup_percent = Float(required=False, description=_("markup percentage")) markup_percent = Float(required=False, description=_("markup percentage"))
filterable_attributes = List( filterable_attributes = List(
NonNull(FilterableAttributeType), NonNull(FilterableAttributeType),
description=_( description=_("which attributes and values can be used for filtering this category."),
"which attributes and values can be used for filtering this category."
),
) )
min_max_prices = Field( min_max_prices = Field(
NonNull(MinMaxPriceType), NonNull(MinMaxPriceType),
description=_( description=_("minimum and maximum prices for products in this category, if available."),
"minimum and maximum prices for products in this category, if available."
),
)
tags = DjangoFilterConnectionField(
lambda: CategoryTagType, description=_("tags for this category")
)
products = DjangoFilterConnectionField(
lambda: ProductType, description=_("products in this category")
) )
tags = DjangoFilterConnectionField(lambda: CategoryTagType, description=_("tags for this category"))
products = DjangoFilterConnectionField(lambda: ProductType, description=_("products in this category"))
class Meta: class Meta:
model = Category model = Category
@ -223,9 +209,7 @@ class CategoryType(DjangoObjectType):
) )
min_max_prices["min_price"] = price_aggregation.get("min_price", 0.0) min_max_prices["min_price"] = price_aggregation.get("min_price", 0.0)
min_max_prices["max_price"] = price_aggregation.get("max_price", 0.0) min_max_prices["max_price"] = price_aggregation.get("max_price", 0.0)
cache.set( cache.set(key=f"{self.name}_min_max_prices", value=min_max_prices, timeout=86400)
key=f"{self.name}_min_max_prices", value=min_max_prices, timeout=86400
)
return { return {
"min_price": min_max_prices["min_price"], "min_price": min_max_prices["min_price"],
@ -275,9 +259,7 @@ class AddressType(DjangoObjectType):
class FeedbackType(DjangoObjectType): class FeedbackType(DjangoObjectType):
comment = String(description=_("comment")) comment = String(description=_("comment"))
rating = Int( rating = Int(description=_("rating value from 1 to 10, inclusive, or 0 if not set."))
description=_("rating value from 1 to 10, inclusive, or 0 if not set.")
)
class Meta: class Meta:
model = Feedback model = Feedback
@ -290,9 +272,7 @@ class FeedbackType(DjangoObjectType):
class OrderProductType(DjangoObjectType): class OrderProductType(DjangoObjectType):
attributes = GenericScalar(description=_("attributes")) attributes = GenericScalar(description=_("attributes"))
notifications = GenericScalar(description=_("notifications")) notifications = GenericScalar(description=_("notifications"))
download_url = String( download_url = String(description=_("download url for this order product if applicable"))
description=_("download url for this order product if applicable")
)
class Meta: class Meta:
model = OrderProduct model = OrderProduct
@ -326,9 +306,7 @@ class OrderType(DjangoObjectType):
billing_address = Field(AddressType, description=_("billing address")) billing_address = Field(AddressType, description=_("billing address"))
shipping_address = Field( shipping_address = Field(
AddressType, AddressType,
description=_( description=_("shipping address for this order, leave blank if same as billing address or if not applicable"),
"shipping address for this order, leave blank if same as billing address or if not applicable"
),
) )
total_price = Float(description=_("total price of this order")) total_price = Float(description=_("total price of this order"))
total_quantity = Int(description=_("total quantity of products in order")) total_quantity = Int(description=_("total quantity of products in order"))
@ -386,9 +364,7 @@ class ProductType(DjangoObjectType):
images = DjangoFilterConnectionField(ProductImageType, description=_("images")) images = DjangoFilterConnectionField(ProductImageType, description=_("images"))
feedbacks = DjangoFilterConnectionField(FeedbackType, description=_("feedbacks")) feedbacks = DjangoFilterConnectionField(FeedbackType, description=_("feedbacks"))
brand = Field(BrandType, description=_("brand")) brand = Field(BrandType, description=_("brand"))
attribute_groups = DjangoFilterConnectionField( attribute_groups = DjangoFilterConnectionField(AttributeGroupType, description=_("attribute groups"))
AttributeGroupType, description=_("attribute groups")
)
price = Float(description=_("price")) price = Float(description=_("price"))
quantity = Float(description=_("quantity")) quantity = Float(description=_("quantity"))
feedbacks_count = Int(description=_("number of feedbacks")) feedbacks_count = Int(description=_("number of feedbacks"))
@ -425,9 +401,7 @@ class ProductType(DjangoObjectType):
def resolve_attribute_groups(self: Product, info): def resolve_attribute_groups(self: Product, info):
info.context._product_uuid = self.uuid info.context._product_uuid = self.uuid
return AttributeGroup.objects.filter( return AttributeGroup.objects.filter(attributes__values__product=self).distinct()
attributes__values__product=self
).distinct()
def resolve_quantity(self, _info) -> int: def resolve_quantity(self, _info) -> int:
return self.quantity or 0 return self.quantity or 0
@ -462,20 +436,14 @@ class PromoCodeType(DjangoObjectType):
description = _("promocodes") description = _("promocodes")
def resolve_discount(self: PromoCode, _info) -> float: def resolve_discount(self: PromoCode, _info) -> float:
return ( return float(self.discount_percent) if self.discount_percent else float(self.discount_amount)
float(self.discount_percent)
if self.discount_percent
else float(self.discount_amount)
)
def resolve_discount_type(self: PromoCode, _info) -> str: def resolve_discount_type(self: PromoCode, _info) -> str:
return "percent" if self.discount_percent else "amount" return "percent" if self.discount_percent else "amount"
class PromotionType(DjangoObjectType): class PromotionType(DjangoObjectType):
products = DjangoFilterConnectionField( products = DjangoFilterConnectionField(ProductType, description=_("products on sale"))
ProductType, description=_("products on sale")
)
class Meta: class Meta:
model = Promotion model = Promotion
@ -498,9 +466,7 @@ class StockType(DjangoObjectType):
class WishlistType(DjangoObjectType): class WishlistType(DjangoObjectType):
products = DjangoFilterConnectionField( products = DjangoFilterConnectionField(ProductType, description=_("wishlisted products"))
ProductType, description=_("wishlisted products")
)
class Meta: class Meta:
model = Wishlist model = Wishlist
@ -510,9 +476,7 @@ class WishlistType(DjangoObjectType):
class ProductTagType(DjangoObjectType): class ProductTagType(DjangoObjectType):
product_set = DjangoFilterConnectionField( product_set = DjangoFilterConnectionField(ProductType, description=_("tagged products"))
ProductType, description=_("tagged products")
)
class Meta: class Meta:
model = ProductTag model = ProductTag
@ -523,9 +487,7 @@ class ProductTagType(DjangoObjectType):
class CategoryTagType(DjangoObjectType): class CategoryTagType(DjangoObjectType):
category_set = DjangoFilterConnectionField( category_set = DjangoFilterConnectionField(CategoryType, description=_("tagged categories"))
CategoryType, description=_("tagged categories")
)
class Meta: class Meta:
model = CategoryTag model = CategoryTag
@ -541,11 +503,7 @@ class ConfigType(ObjectType):
company_name = String(description=_("company name")) company_name = String(description=_("company name"))
company_address = String(description=_("company address")) company_address = String(description=_("company address"))
company_phone_number = String(description=_("company phone number")) company_phone_number = String(description=_("company phone number"))
email_from = String( email_from = String(description=_("email from, sometimes it must be used instead of host user value"))
description=_(
"email from, sometimes it must be used instead of host user value"
)
)
email_host_user = String(description=_("email host user")) email_host_user = String(description=_("email host user"))
payment_gateway_maximum = Float(description=_("maximum amount for payment")) payment_gateway_maximum = Float(description=_("maximum amount for payment"))
payment_gateway_minimum = Float(description=_("minimum amount for payment")) payment_gateway_minimum = Float(description=_("minimum amount for payment"))
@ -593,15 +551,9 @@ class SearchPostsResultsType(ObjectType):
class SearchResultsType(ObjectType): class SearchResultsType(ObjectType):
products = List( products = List(description=_("products search results"), of_type=SearchProductsResultsType)
description=_("products search results"), of_type=SearchProductsResultsType categories = List(description=_("products search results"), of_type=SearchCategoriesResultsType)
) brands = List(description=_("products search results"), of_type=SearchBrandsResultsType)
categories = List(
description=_("products search results"), of_type=SearchCategoriesResultsType
)
brands = List(
description=_("products search results"), of_type=SearchBrandsResultsType
)
posts = List(description=_("posts search results"), of_type=SearchPostsResultsType) posts = List(description=_("posts search results"), of_type=SearchPostsResultsType)

View file

@ -79,7 +79,9 @@ def load_po_sanitized(path: str) -> polib.POFile | None:
rest = parts[1] if len(parts) > 1 else "" rest = parts[1] if len(parts) > 1 else ""
rest_clean = re.sub(r"^msgid \"\"\s*\nmsgstr \"\"\s*\n?", "", rest, flags=re.MULTILINE) rest_clean = re.sub(r"^msgid \"\"\s*\nmsgstr \"\"\s*\n?", "", rest, flags=re.MULTILINE)
sanitized = header + "\n\n" + rest_clean sanitized = header + "\n\n" + rest_clean
tmp = NamedTemporaryFile(mode="w+", delete=False, suffix=".po", encoding="utf-8") # noqa: SIM115 tmp = NamedTemporaryFile( # noqa: SIM115
mode="w+", delete=False, suffix=".po", encoding="utf-8"
)
try: try:
tmp.write(sanitized) tmp.write(sanitized)
tmp.flush() tmp.flush()
@ -163,7 +165,12 @@ class Command(BaseCommand):
entries = [e for e in en_po if e.msgid and not e.obsolete] entries = [e for e in en_po if e.msgid and not e.obsolete]
source_map = {e.msgid: e.msgstr for e in entries} source_map = {e.msgid: e.msgstr for e in entries}
tgt_dir = os.path.join(app_conf.path, "locale", target_lang.replace("-", "_"), "LC_MESSAGES") tgt_dir = os.path.join(
app_conf.path,
"locale",
target_lang.replace("-", "_"),
"LC_MESSAGES",
)
os.makedirs(tgt_dir, exist_ok=True) os.makedirs(tgt_dir, exist_ok=True)
tgt_path = os.path.join(tgt_dir, "django.po") tgt_path = os.path.join(tgt_dir, "django.po")
@ -221,7 +228,7 @@ class Command(BaseCommand):
if len(trans) != len(to_trans): if len(trans) != len(to_trans):
raise CommandError(f"Got {len(trans)} translations, expected {len(to_trans)}") raise CommandError(f"Got {len(trans)} translations, expected {len(to_trans)}")
for e, obj, pmap in zip(to_trans, trans, maps): for e, obj, pmap in zip(to_trans, trans, maps, strict=True):
e.msgstr = deplaceholderize(obj["text"], pmap) e.msgstr = deplaceholderize(obj["text"], pmap)
new_po.save(tgt_path) new_po.save(tgt_path)

View file

@ -11,17 +11,10 @@ class Command(BaseCommand):
def reset_em(self, queryset): def reset_em(self, queryset):
total = queryset.count() total = queryset.count()
self.stdout.write( self.stdout.write(f"Starting slug rebuilding for {total} {queryset.model._meta.verbose_name_plural}")
f"Starting slug rebuilding for {total} {queryset.model._meta.verbose_name_plural}"
)
for idx, instance in enumerate(queryset.iterator(), start=1): for idx, instance in enumerate(queryset.iterator(), start=1):
try: try:
while ( while queryset.filter(name=instance.name).exclude(uuid=instance.uuid).count() >= 1:
queryset.filter(name=instance.name)
.exclude(uuid=instance.uuid)
.count()
>= 1
):
instance.name = f"{instance.name} - {get_random_string(length=3, allowed_chars='0123456789')}" instance.name = f"{instance.name} - {get_random_string(length=3, allowed_chars='0123456789')}"
instance.save() instance.save()
instance.slug = None instance.slug = None
@ -43,7 +36,6 @@ class Command(BaseCommand):
) )
def handle(self, *args, **options): def handle(self, *args, **options):
for queryset in [ for queryset in [
Brand.objects.all(), Brand.objects.all(),
Category.objects.all(), Category.objects.all(),

View file

@ -50,6 +50,7 @@ from core.utils import (
get_product_uuid_as_path, get_product_uuid_as_path,
get_random_code, get_random_code,
) )
from core.utils.db import TweakedAutoSlugField
from core.utils.lists import FAILED_STATUSES from core.utils.lists import FAILED_STATUSES
from core.validators import validate_category_image_dimensions from core.validators import validate_category_image_dimensions
from evibes.settings import CURRENCY_CODE from evibes.settings import CURRENCY_CODE
@ -91,9 +92,7 @@ class Vendor(ExportModelOperationsMixin("vendor"), NiceModel):
authentication: dict = JSONField( # type: ignore authentication: dict = JSONField( # type: ignore
blank=True, blank=True,
null=True, null=True,
help_text=_( help_text=_("stores credentials and endpoints required for vendor communication"),
"stores credentials and endpoints required for vendor communication"
),
verbose_name=_("authentication info"), verbose_name=_("authentication info"),
) )
markup_percent: int = IntegerField( # type: ignore markup_percent: int = IntegerField( # type: ignore
@ -213,9 +212,9 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel):
verbose_name=_("category description"), verbose_name=_("category description"),
) )
slug: str = AutoSlugField( # type: ignore slug: str = TweakedAutoSlugField( # type: ignore
populate_from=( populate_from=(
"uuid", "parent__name",
"name", "name",
), ),
allow_unicode=True, allow_unicode=True,
@ -238,10 +237,7 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel):
def get_tree_depth(self): def get_tree_depth(self):
if self.is_leaf_node(): if self.is_leaf_node():
return 0 return 0
return ( return self.get_descendants().aggregate(max_depth=Max("level"))["max_depth"] - self.get_level()
self.get_descendants().aggregate(max_depth=Max("level"))["max_depth"]
- self.get_level()
)
class Meta: class Meta:
verbose_name = _("category") verbose_name = _("category")
@ -402,9 +398,7 @@ class Product(ExportModelOperationsMixin("product"), NiceModel):
cache_key = f"product_feedbacks_count_{self.pk}" cache_key = f"product_feedbacks_count_{self.pk}"
feedbacks_count = cache.get(cache_key) feedbacks_count = cache.get(cache_key)
if feedbacks_count is None: if feedbacks_count is None:
feedbacks_count = Feedback.objects.filter( feedbacks_count = Feedback.objects.filter(order_product__product_id=self.pk).count()
order_product__product_id=self.pk
).count()
cache.set(cache_key, feedbacks_count, 604800) cache.set(cache_key, feedbacks_count, 604800)
return feedbacks_count return feedbacks_count
@ -703,9 +697,7 @@ class Wishlist(ExportModelOperationsMixin("wishlist"), NiceModel):
class Documentary(ExportModelOperationsMixin("attribute_group"), NiceModel): class Documentary(ExportModelOperationsMixin("attribute_group"), NiceModel):
is_publicly_visible = True is_publicly_visible = True
product: ForeignKey = ForeignKey( product: ForeignKey = ForeignKey(to=Product, on_delete=CASCADE, related_name="documentaries")
to=Product, on_delete=CASCADE, related_name="documentaries"
)
document = FileField(upload_to=get_product_uuid_as_path) document = FileField(upload_to=get_product_uuid_as_path)
class Meta: class Meta:
@ -833,9 +825,7 @@ class PromoCode(ExportModelOperationsMixin("promocode"), NiceModel):
self.discount_amount is None and self.discount_percent is None self.discount_amount is None and self.discount_percent is None
): ):
raise ValidationError( raise ValidationError(
_( _("only one type of discount should be defined (amount or percent), but not both or neither.")
"only one type of discount should be defined (amount or percent), but not both or neither."
)
) )
super().save(**kwargs) super().save(**kwargs)
@ -856,15 +846,11 @@ class PromoCode(ExportModelOperationsMixin("promocode"), NiceModel):
if self.discount_type == "percent": if self.discount_type == "percent":
amount -= round(amount * (self.discount_percent / 100), 2) amount -= round(amount * (self.discount_percent / 100), 2)
order.attributes.update( order.attributes.update({"promocode": str(self.uuid), "final_price": amount})
{"promocode": str(self.uuid), "final_price": amount}
)
order.save() order.save()
elif self.discount_type == "amount": elif self.discount_type == "amount":
amount -= round(float(self.discount_amount), 2) amount -= round(float(self.discount_amount), 2)
order.attributes.update( order.attributes.update({"promocode": str(self.uuid), "final_price": amount})
{"promocode": str(self.uuid), "final_price": amount}
)
order.save() order.save()
else: else:
raise ValueError(_(f"invalid discount type for promocode {self.uuid}")) raise ValueError(_(f"invalid discount type for promocode {self.uuid}"))
@ -972,8 +958,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
sum( sum(
( (
order_product.buy_price * order_product.quantity order_product.buy_price * order_product.quantity
if order_product.status not in FAILED_STATUSES if order_product.status not in FAILED_STATUSES and order_product.buy_price is not None
and order_product.buy_price is not None
else 0.0 else 0.0
) )
for order_product in self.order_products.all() for order_product in self.order_products.all()
@ -990,16 +975,14 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
def add_product( def add_product(
self, self,
product_uuid: str | None = None, product_uuid: str | None = None,
attributes: Optional[list] = None, attributes: list | None = None,
update_quantity: bool = True, update_quantity: bool = True,
): ):
if attributes is None: if attributes is None:
attributes = [] attributes = []
if self.status not in ["PENDING", "MOMENTAL"]: if self.status not in ["PENDING", "MOMENTAL"]:
raise ValueError( raise ValueError(_("you cannot add products to an order that is not a pending one"))
_("you cannot add products to an order that is not a pending one")
)
try: try:
product = Product.objects.get(uuid=product_uuid) product = Product.objects.get(uuid=product_uuid)
@ -1008,9 +991,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
buy_price = product.price buy_price = product.price
promotions = Promotion.objects.filter( promotions = Promotion.objects.filter(is_active=True, products__in=[product]).order_by("discount_percent")
is_active=True, products__in=[product]
).order_by("discount_percent")
if promotions.exists(): if promotions.exists():
buy_price -= round(product.price * (promotions.first().discount_percent / 100), 2) # type: ignore buy_price -= round(product.price * (promotions.first().discount_percent / 100), 2) # type: ignore
@ -1023,9 +1004,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
) )
if not is_created and update_quantity: if not is_created and update_quantity:
if product.quantity < order_product.quantity + 1: if product.quantity < order_product.quantity + 1:
raise BadRequest( raise BadRequest(_("you cannot add more products than available in stock"))
_("you cannot add more products than available in stock")
)
order_product.quantity += 1 order_product.quantity += 1
order_product.buy_price = product.price order_product.buy_price = product.price
order_product.save() order_product.save()
@ -1046,9 +1025,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
attributes = {} attributes = {}
if self.status not in ["PENDING", "MOMENTAL"]: if self.status not in ["PENDING", "MOMENTAL"]:
raise ValueError( raise ValueError(_("you cannot remove products from an order that is not a pending one"))
_("you cannot remove products from an order that is not a pending one")
)
try: try:
product = Product.objects.get(uuid=product_uuid) product = Product.objects.get(uuid=product_uuid)
order_product = self.order_products.get(product=product, order=self) order_product = self.order_products.get(product=product, order=self)
@ -1067,16 +1044,12 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
raise Http404(_(f"{name} does not exist: {product_uuid}")) raise Http404(_(f"{name} does not exist: {product_uuid}"))
except OrderProduct.DoesNotExist: except OrderProduct.DoesNotExist:
name = "OrderProduct" name = "OrderProduct"
query = ( query = f"product: {product_uuid}, order: {self.uuid}, attributes: {attributes}"
f"product: {product_uuid}, order: {self.uuid}, attributes: {attributes}"
)
raise Http404(_(f"{name} does not exist with query <{query}>")) raise Http404(_(f"{name} does not exist with query <{query}>"))
def remove_all_products(self): def remove_all_products(self):
if self.status not in ["PENDING", "MOMENTAL"]: if self.status not in ["PENDING", "MOMENTAL"]:
raise ValueError( raise ValueError(_("you cannot remove products from an order that is not a pending one"))
_("you cannot remove products from an order that is not a pending one")
)
for order_product in self.order_products.all(): for order_product in self.order_products.all():
self.order_products.remove(order_product) self.order_products.remove(order_product)
order_product.delete() order_product.delete()
@ -1084,9 +1057,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
def remove_products_of_a_kind(self, product_uuid: str): def remove_products_of_a_kind(self, product_uuid: str):
if self.status not in ["PENDING", "MOMENTAL"]: if self.status not in ["PENDING", "MOMENTAL"]:
raise ValueError( raise ValueError(_("you cannot remove products from an order that is not a pending one"))
_("you cannot remove products from an order that is not a pending one")
)
try: try:
product = Product.objects.get(uuid=product_uuid) product = Product.objects.get(uuid=product_uuid)
order_product = self.order_products.get(product=product, order=self) order_product = self.order_products.get(product=product, order=self)
@ -1099,10 +1070,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
@property @property
def is_whole_digital(self): def is_whole_digital(self):
return ( return self.order_products.count() == self.order_products.filter(product__is_digital=True).count()
self.order_products.count()
== self.order_products.filter(product__is_digital=True).count()
)
def apply_promocode(self, promocode_uuid: str): def apply_promocode(self, promocode_uuid: str):
try: try:
@ -1117,11 +1085,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
if self.is_whole_digital: if self.is_whole_digital:
return return
else: else:
raise ValueError( raise ValueError(_("you can only buy physical products with shipping address specified"))
_(
"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:
shipping_address = Address.objects.get(uuid=billing_address_uuid) shipping_address = Address.objects.get(uuid=billing_address_uuid)
@ -1151,13 +1115,9 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
shipping_address: str | None = None, shipping_address: str | None = None,
) -> Self | Transaction | None: ) -> Self | Transaction | None:
if config.DISABLED_COMMERCE: if config.DISABLED_COMMERCE:
raise DisabledCommerceError( raise DisabledCommerceError(_("you can not buy at this moment, please try again in a few minutes"))
_("you can not buy at this moment, please try again in a few minutes")
)
if (not force_balance and not force_payment) or ( if (not force_balance and not force_payment) or (force_balance and force_payment):
force_balance and force_payment
):
raise ValueError(_("invalid force value")) raise ValueError(_("invalid force value"))
self.apply_addresses(billing_address, shipping_address) self.apply_addresses(billing_address, shipping_address)
@ -1173,16 +1133,12 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
if force_payment: if force_payment:
force = "payment" force = "payment"
amount = ( amount = self.apply_promocode(promocode_uuid) if promocode_uuid else self.total_price
self.apply_promocode(promocode_uuid) if promocode_uuid else self.total_price
)
match force: match force:
case "balance": case "balance":
if self.user.payments_balance.amount < amount: # type: ignore if self.user.payments_balance.amount < amount: # type: ignore
raise NotEnoughMoneyError( raise NotEnoughMoneyError(_("insufficient funds to complete the order"))
_("insufficient funds to complete the order")
)
self.status = "CREATED" self.status = "CREATED"
self.buy_time = timezone.now() self.buy_time = timezone.now()
self.order_products.all().update(status="DELIVERING") self.order_products.all().update(status="DELIVERING")
@ -1200,13 +1156,9 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
return self return self
def buy_without_registration( def buy_without_registration(self, products: list, promocode_uuid: str, **kwargs) -> Transaction | None:
self, products: list, promocode_uuid: str, **kwargs
) -> Transaction | None:
if config.DISABLED_COMMERCE: if config.DISABLED_COMMERCE:
raise DisabledCommerceError( raise DisabledCommerceError(_("you can not buy at this moment, please try again in a few minutes"))
_("you can not buy at this moment, please try again in a few minutes")
)
if len(products) < 1: if len(products) < 1:
raise ValueError(_("you cannot purchase an empty order!")) raise ValueError(_("you cannot purchase an empty order!"))
@ -1227,25 +1179,17 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
available_payment_methods = cache.get("payment_methods").get("payment_methods") available_payment_methods = cache.get("payment_methods").get("payment_methods")
if payment_method not in available_payment_methods: if payment_method not in available_payment_methods:
raise ValueError( raise ValueError(_(f"invalid payment method: {payment_method} from {available_payment_methods}"))
_(
f"invalid payment method: {payment_method} from {available_payment_methods}"
)
)
billing_customer_address_uuid = kwargs.get("billing_customer_address") billing_customer_address_uuid = kwargs.get("billing_customer_address")
shipping_customer_address_uuid = kwargs.get("shipping_customer_address") shipping_customer_address_uuid = kwargs.get("shipping_customer_address")
self.apply_addresses( self.apply_addresses(billing_customer_address_uuid, shipping_customer_address_uuid)
billing_customer_address_uuid, shipping_customer_address_uuid
)
for product_uuid in products: for product_uuid in products:
self.add_product(product_uuid) self.add_product(product_uuid)
amount = ( amount = self.apply_promocode(promocode_uuid) if promocode_uuid else self.total_price
self.apply_promocode(promocode_uuid) if promocode_uuid else self.total_price
)
self.status = "CREATED" self.status = "CREATED"
@ -1390,9 +1334,7 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel):
"errors": [ "errors": [
{ {
"detail": ( "detail": (
error error if error else f"Something went wrong with {self.uuid} for some reason..."
if error
else f"Something went wrong with {self.uuid} for some reason..."
) )
}, },
] ]
@ -1417,9 +1359,7 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel):
return self.download.url return self.download.url
return "" return ""
def do_feedback( def do_feedback(self, rating: int = 10, comment: str = "", action: str = "add") -> Optional["Feedback"]:
self, rating: int = 10, comment: str = "", action: str = "add"
) -> Optional["Feedback"]:
if action not in ["add", "remove"]: if action not in ["add", "remove"]:
raise ValueError(_(f"wrong action specified for feedback: {action}")) raise ValueError(_(f"wrong action specified for feedback: {action}"))
if action == "remove" and self.feedback: if action == "remove" and self.feedback:
@ -1427,13 +1367,9 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel):
return None return None
if action == "add" and not self.feedback: if action == "add" and not self.feedback:
if self.order.status not in ["MOMENTAL", "PENDING"]: # type: ignore if self.order.status not in ["MOMENTAL", "PENDING"]: # type: ignore
return Feedback.objects.create( return Feedback.objects.create(rating=rating, comment=comment, order_product=self)
rating=rating, comment=comment, order_product=self
)
else: else:
raise ValueError( raise ValueError(_("you cannot feedback an order which is not received"))
_("you cannot feedback an order which is not received")
)
return None return None
@ -1453,11 +1389,11 @@ class DigitalAssetDownload(ExportModelOperationsMixin("attribute_group"), NiceMo
@property @property
def url(self): def url(self):
if self.order_product.status != "FINISHED": if self.order_product.status != "FINISHED":
raise ValueError( raise ValueError(_("you can not download a digital asset for a non-finished order"))
_("you can not download a digital asset for a non-finished order")
)
return f"https://api.{config.BASE_DOMAIN}/download/{urlsafe_base64_encode(force_bytes(self.order_product.uuid))}" return (
f"https://api.{config.BASE_DOMAIN}/download/{urlsafe_base64_encode(force_bytes(self.order_product.uuid))}"
)
class Feedback(ExportModelOperationsMixin("feedback"), NiceModel): class Feedback(ExportModelOperationsMixin("feedback"), NiceModel):
@ -1474,9 +1410,7 @@ class Feedback(ExportModelOperationsMixin("feedback"), NiceModel):
on_delete=CASCADE, on_delete=CASCADE,
blank=False, blank=False,
null=False, null=False,
help_text=_( help_text=_("references the specific product in an order that this feedback is about"),
"references the specific product in an order that this feedback is about"
),
verbose_name=_("related order product"), verbose_name=_("related order product"),
) )
rating: float = FloatField( # type: ignore rating: float = FloatField( # type: ignore

View file

@ -1,6 +1,7 @@
import logging import logging
from collections.abc import Collection
from contextlib import suppress from contextlib import suppress
from typing import Any, Collection, Optional from typing import Any
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.core.cache import cache from django.core.cache import cache
@ -69,7 +70,7 @@ class CategoryDetailSerializer(ModelSerializer):
"modified", "modified",
] ]
def get_image(self, obj: Category) -> Optional[str]: def get_image(self, obj: Category) -> str | None:
with suppress(ValueError): with suppress(ValueError):
return obj.image.url return obj.image.url
return None return None
@ -148,12 +149,12 @@ class BrandDetailSerializer(ModelSerializer):
"small_logo", "small_logo",
] ]
def get_small_logo(self, obj: Brand) -> Optional[str]: def get_small_logo(self, obj: Brand) -> str | None:
with suppress(ValueError): with suppress(ValueError):
return obj.small_logo.url return obj.small_logo.url
return None return None
def get_big_logo(self, obj: Brand) -> Optional[str]: def get_big_logo(self, obj: Brand) -> str | None:
with suppress(ValueError): with suppress(ValueError):
return obj.big_logo.url return obj.big_logo.url
return None return None
@ -174,12 +175,12 @@ class BrandProductDetailSerializer(ModelSerializer):
"small_logo", "small_logo",
] ]
def get_small_logo(self, obj: Brand) -> Optional[str]: def get_small_logo(self, obj: Brand) -> str | None:
with suppress(ValueError): with suppress(ValueError):
return obj.small_logo.url return obj.small_logo.url
return None return None
def get_big_logo(self, obj: Brand) -> Optional[str]: def get_big_logo(self, obj: Brand) -> str | None:
with suppress(ValueError): with suppress(ValueError):
return obj.big_logo.url return obj.big_logo.url
return None return None

View file

@ -1,5 +1,5 @@
from collections.abc import Collection
from contextlib import suppress from contextlib import suppress
from typing import Collection, Optional
from rest_framework.fields import JSONField, SerializerMethodField from rest_framework.fields import JSONField, SerializerMethodField
from rest_framework.relations import PrimaryKeyRelatedField from rest_framework.relations import PrimaryKeyRelatedField
@ -54,7 +54,7 @@ class CategorySimpleSerializer(ModelSerializer):
"children", "children",
] ]
def get_image(self, obj: Category) -> Optional[str]: def get_image(self, obj: Category) -> str | None:
with suppress(ValueError): with suppress(ValueError):
return obj.image.url return obj.image.url
return None return None
@ -89,12 +89,12 @@ class BrandSimpleSerializer(ModelSerializer):
"big_logo", "big_logo",
] ]
def get_small_logo(self, obj: Brand) -> Optional[str]: def get_small_logo(self, obj: Brand) -> str | None:
with suppress(ValueError): with suppress(ValueError):
return obj.small_logo.url return obj.small_logo.url
return None return None
def get_big_logo(self, obj: Brand) -> Optional[str]: def get_big_logo(self, obj: Brand) -> str | None:
with suppress(ValueError): with suppress(ValueError):
return obj.big_logo.url return obj.big_logo.url
return None return None

View file

@ -33,9 +33,7 @@ def create_order_on_user_creation_signal(instance, created, **_kwargs):
if Order.objects.filter(human_readable_id=human_readable_id).exists(): if Order.objects.filter(human_readable_id=human_readable_id).exists():
human_readable_id = generate_human_readable_id() human_readable_id = generate_human_readable_id()
continue continue
Order.objects.create( Order.objects.create(user=instance, status="PENDING", human_readable_id=human_readable_id)
user=instance, status="PENDING", human_readable_id=human_readable_id
)
break break
@ -49,9 +47,7 @@ def create_wishlist_on_user_creation_signal(instance, created, **_kwargs):
def create_promocode_on_user_referring(instance, created, **_kwargs): def create_promocode_on_user_referring(instance, created, **_kwargs):
try: try:
if created and instance.attributes.get("referrer", ""): if created and instance.attributes.get("referrer", ""):
referrer_uuid = urlsafe_base64_decode( referrer_uuid = urlsafe_base64_decode(instance.attributes.get("referrer", ""))
instance.attributes.get("referrer", "")
)
referrer = User.objects.get(uuid=referrer_uuid) referrer = User.objects.get(uuid=referrer_uuid)
code = f"WELCOME-{get_random_string(6)}" code = f"WELCOME-{get_random_string(6)}"
PromoCode.objects.create( PromoCode.objects.create(
@ -78,9 +74,7 @@ def process_order_changes(instance, created, **_kwargs):
except IntegrityError: except IntegrityError:
human_readable_id = generate_human_readable_id() human_readable_id = generate_human_readable_id()
while True: while True:
if Order.objects.filter( if Order.objects.filter(human_readable_id=human_readable_id).exists():
human_readable_id=human_readable_id
).exists():
human_readable_id = generate_human_readable_id() human_readable_id = generate_human_readable_id()
continue continue
Order.objects.create( Order.objects.create(
@ -100,31 +94,20 @@ def process_order_changes(instance, created, **_kwargs):
try: try:
vendor_name = ( vendor_name = (
order_product.product.stocks.filter( order_product.product.stocks.filter(price=order_product.buy_price).first().vendor.name.lower()
price=order_product.buy_price
)
.first()
.vendor.name.lower()
) )
vendor = create_object( vendor = create_object(f"core.vendors.{vendor_name}", f"{vendor_name.title()}Vendor")
f"core.vendors.{vendor_name}", f"{vendor_name.title()}Vendor"
)
vendor.buy_order_product(order_product) vendor.buy_order_product(order_product)
except Exception as e: except Exception as e:
order_product.add_error( order_product.add_error(f"Failed to buy {order_product.uuid}. Reason: {e}...")
f"Failed to buy {order_product.uuid}. Reason: {e}..."
)
else: else:
instance.finalize() instance.finalize()
if ( if instance.order_products.filter(status="FAILED").count() == instance.order_products.count():
instance.order_products.filter(status="FAILED").count()
== instance.order_products.count()
):
instance.status = "FAILED" instance.status = "FAILED"
instance.save() instance.save()

View file

@ -1,5 +1,7 @@
from django.db.models import Model from django.db.models import Model
from django.db.models.constants import LOOKUP_SEP
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_extensions.db.fields import AutoSlugField
def list_to_queryset(model: Model, data: list): def list_to_queryset(model: Model, data: list):
@ -10,3 +12,21 @@ def list_to_queryset(model: Model, data: list):
pk_list = [obj.pk for obj in data] pk_list = [obj.pk for obj in data]
return model.objects.filter(pk__in=pk_list) return model.objects.filter(pk__in=pk_list)
class TweakedAutoSlugField(AutoSlugField):
def get_slug_fields(self, model_instance, lookup_value):
if callable(lookup_value):
return f"{lookup_value(model_instance)}"
lookup_value_path = lookup_value.split(LOOKUP_SEP)
attr = model_instance
for elem in lookup_value_path:
try:
attr = getattr(attr, elem)
except AttributeError:
attr = ""
if callable(attr):
return f"{attr()}"
return attr

View file

@ -1,11 +1,9 @@
from typing import Dict, List
import requests import requests
from constance import config from constance import config
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
def fetch_address_suggestions(query: str, limit: int = 5) -> List[Dict]: def fetch_address_suggestions(query: str, limit: int = 5) -> list[dict]:
if not config.NOMINATIM_URL: if not config.NOMINATIM_URL:
raise ValueError(_("NOMINATIM_URL must be configured.")) raise ValueError(_("NOMINATIM_URL must be configured."))

View file

@ -145,23 +145,17 @@ class AbstractVendor:
return value, "string" return value, "string"
@staticmethod @staticmethod
def auto_resolver_helper( def auto_resolver_helper(model: Brand | Category, resolving_name: str) -> Brand | Category | None:
model: Brand | Category, resolving_name: str
) -> Brand | Category | None:
queryset = model.objects.filter(name=resolving_name) queryset = model.objects.filter(name=resolving_name)
if not queryset.exists(): if not queryset.exists():
return model.objects.get_or_create( return model.objects.get_or_create(name=resolving_name, defaults={"is_active": False})[0]
name=resolving_name, defaults={"is_active": False}
)[0]
elif queryset.filter(is_active=True).count() > 1: elif queryset.filter(is_active=True).count() > 1:
queryset = queryset.filter(is_active=True) queryset = queryset.filter(is_active=True)
elif queryset.filter(is_active=False).count() > 1: elif queryset.filter(is_active=False).count() > 1:
queryset = queryset.filter(is_active=False) queryset = queryset.filter(is_active=False)
chosen = queryset.first() chosen = queryset.first()
if not chosen: if not chosen:
raise VendorError( raise VendorError(f"No matching {model.__name__} found with name {resolving_name!r}...")
f"No matching {model.__name__} found with name {resolving_name!r}..."
)
queryset = queryset.exclude(uuid=chosen.uuid) queryset = queryset.exclude(uuid=chosen.uuid)
queryset.delete() queryset.delete()
return chosen return chosen
@ -227,9 +221,7 @@ class AbstractVendor:
rate = rates.get(currency or self.currency) rate = rates.get(currency or self.currency)
if not rate: if not rate:
raise RatesError( raise RatesError(f"No rate found for {currency or self.currency} in {rates} with probider {provider}...")
f"No rate found for {currency or self.currency} in {rates} with probider {provider}..."
)
return round(price / rate, 2) if rate else round(price, 2) return round(price / rate, 2) if rate else round(price, 2)
@ -273,22 +265,16 @@ class AbstractVendor:
return vendor return vendor
raise VendorError(f"Vendor {self.vendor_name!r} is inactive...") raise VendorError(f"Vendor {self.vendor_name!r} is inactive...")
except Vendor.DoesNotExist: except Vendor.DoesNotExist:
raise Exception( raise Exception(f"No matching vendor found with name {self.vendor_name!r}...")
f"No matching vendor found with name {self.vendor_name!r}..."
)
def get_products(self): def get_products(self):
pass pass
def get_products_queryset(self): def get_products_queryset(self):
return Product.objects.filter( return Product.objects.filter(stocks__vendor=self.get_vendor_instance(), orderproduct__isnull=True)
stocks__vendor=self.get_vendor_instance(), orderproduct__isnull=True
)
def get_stocks_queryset(self): def get_stocks_queryset(self):
return Stock.objects.filter( return Stock.objects.filter(product__in=self.get_products_queryset(), product__orderproduct__isnull=True)
product__in=self.get_products_queryset(), product__orderproduct__isnull=True
)
def get_attribute_values_queryset(self): def get_attribute_values_queryset(self):
return AttributeValue.objects.filter( return AttributeValue.objects.filter(
@ -306,9 +292,7 @@ class AbstractVendor:
self.get_stocks_queryset().delete() self.get_stocks_queryset().delete()
self.get_attribute_values_queryset().delete() self.get_attribute_values_queryset().delete()
def process_attribute( def process_attribute(self, key: str, value, product: Product, attr_group: AttributeGroup):
self, key: str, value, product: Product, attr_group: AttributeGroup
):
if not value: if not value:
return return

View file

@ -129,9 +129,7 @@ class WebsiteParametersView(APIView):
] ]
def get(self, request): def get(self, request):
return Response( return Response(data=camelize(get_project_parameters()), status=status.HTTP_200_OK)
data=camelize(get_project_parameters()), status=status.HTTP_200_OK
)
@extend_schema_view(**CACHE_SCHEMA) @extend_schema_view(**CACHE_SCHEMA)
@ -201,9 +199,7 @@ class RequestCursedURLView(APIView):
try: try:
data = cache.get(url, None) data = cache.get(url, None)
if not data: if not data:
response = requests.get( response = requests.get(url, headers={"content-type": "application/json"})
url, headers={"content-type": "application/json"}
)
response.raise_for_status() response.raise_for_status()
data = camelize(response.json()) data = camelize(response.json())
cache.set(url, data, 86400) cache.set(url, data, 86400)
@ -233,15 +229,7 @@ class GlobalSearchView(APIView):
] ]
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return Response( return Response(camelize({"results": process_query(query=request.GET.get("q", "").strip(), request=request)}))
camelize(
{
"results": process_query(
query=request.GET.get("q", "").strip(), request=request
)
}
)
)
@extend_schema_view(**BUY_AS_BUSINESS_SCHEMA) @extend_schema_view(**BUY_AS_BUSINESS_SCHEMA)
@ -251,22 +239,15 @@ class BuyAsBusinessView(APIView):
serializer = BuyAsBusinessOrderSerializer(data=request.data) serializer = BuyAsBusinessOrderSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
order = Order.objects.create(status="MOMENTAL") order = Order.objects.create(status="MOMENTAL")
products = [ products = [product.get("product_uuid") for product in serializer.validated_data.get("products")]
product.get("product_uuid")
for product in serializer.validated_data.get("products")
]
transaction = order.buy_without_registration( transaction = order.buy_without_registration(
products=products, products=products,
promocode_uuid=serializer.validated_data.get("promocode_uuid"), promocode_uuid=serializer.validated_data.get("promocode_uuid"),
customer_name=serializer.validated_data.get("customer_name"), customer_name=serializer.validated_data.get("customer_name"),
customer_email=serializer.validated_data.get("customer_email"), customer_email=serializer.validated_data.get("customer_email"),
customer_phone=serializer.validated_data.get("customer_phone"), customer_phone=serializer.validated_data.get("customer_phone"),
customer_billing_address=serializer.validated_data.get( customer_billing_address=serializer.validated_data.get("customer_billing_address_uuid"),
"customer_billing_address_uuid" customer_shipping_address=serializer.validated_data.get("customer_shipping_address_uuid"),
),
customer_shipping_address=serializer.validated_data.get(
"customer_shipping_address_uuid"
),
payment_method=serializer.validated_data.get("payment_method"), payment_method=serializer.validated_data.get("payment_method"),
is_business=True, is_business=True,
) )
@ -287,9 +268,7 @@ def download_digital_asset_view(request, *args, **kwargs):
download.num_downloads += 1 download.num_downloads += 1
download.save() download.save()
file_path = ( file_path = download.order_product.product.stocks.first().digital_asset.file.path
download.order_product.product.stocks.first().digital_asset.file.path
)
content_type, encoding = mimetypes.guess_type(file_path) content_type, encoding = mimetypes.guess_type(file_path)
if not content_type: if not content_type:

View file

@ -27,7 +27,7 @@ class JSONTableWidget(forms.Widget):
try: try:
keys = data.getlist(f"{name}_key") keys = data.getlist(f"{name}_key")
values = data.getlist(f"{name}_value") values = data.getlist(f"{name}_value")
for key, value in zip(keys, values): for key, value in zip(keys, values, strict=True):
if key.strip(): if key.strip():
try: try:
json_data[key] = json.loads(value) json_data[key] = json.loads(value)

View file

@ -25,9 +25,7 @@ urlpatterns = [
), ),
path( path(
r"docs/", r"docs/",
SpectacularAPIView.as_view( SpectacularAPIView.as_view(urlconf="evibes.api_urls", custom_settings=SPECTACULAR_PLATFORM_SETTINGS),
urlconf="evibes.api_urls", custom_settings=SPECTACULAR_PLATFORM_SETTINGS
),
name="schema-platform", name="schema-platform",
), ),
path( path(

View file

@ -88,10 +88,7 @@ class BlockInvalidHostMiddleware:
allowed_hosts += getenv("ALLOWED_HOSTS").split(" ") allowed_hosts += getenv("ALLOWED_HOSTS").split(" ")
if not hasattr(request, "META"): if not hasattr(request, "META"):
return BadRequest("Invalid Request") return BadRequest("Invalid Request")
if ( if request.META.get("HTTP_HOST") not in allowed_hosts and "*" not in allowed_hosts:
request.META.get("HTTP_HOST") not in allowed_hosts
and "*" not in allowed_hosts
):
return HttpResponseForbidden("Invalid Host Header") return HttpResponseForbidden("Invalid Host Header")
return self.get_response(request) return self.get_response(request)

View file

@ -2,3 +2,5 @@ GRAPH_MODELS = {
"all_applications": True, "all_applications": True,
"group_models": True, "group_models": True,
} }
EXTENSIONS_MAX_UNIQUE_QUERY_ATTEMPTS = 500

View file

@ -20,8 +20,9 @@ def balance_email(user_pk: str) -> tuple[bool, str]:
email = EmailMessage( email = EmailMessage(
"eVibes | Successful Order", "eVibes | Successful Order",
render_to_string("balance_deposit_email.html", render_to_string(
{"user": user, "current_year": timezone.now().year, "config": config}), "balance_deposit_email.html", {"user": user, "current_year": timezone.now().year, "config": config}
),
to=[user.email], to=[user.email],
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>", from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
) )

View file

@ -105,7 +105,7 @@ exclude = ["*/migrations/*", "./evibes/settings/drf.py"]
[tool.ruff] [tool.ruff]
line-length = 120 line-length = 120
target-version = "py38" target-version = "py312"
exclude = ["migrations", "media", "static", "storefront"] exclude = ["migrations", "media", "static", "storefront"]
[tool.ruff.lint] [tool.ruff.lint]

View file

@ -1,6 +1,7 @@
import logging import logging
from collections.abc import Collection
from contextlib import suppress from contextlib import suppress
from typing import Any, Collection, Dict, Optional, Type from typing import Any
from constance import config from constance import config
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
@ -95,15 +96,16 @@ class UserSerializer(ModelSerializer):
Returns a list of serialized ProductSimpleSerializer representations Returns a list of serialized ProductSimpleSerializer representations
for the UUIDs in obj.recently_viewed. for the UUIDs in obj.recently_viewed.
""" """
# noinspection PyTypeChecker # noinspection PytypeChecker
return ProductSimpleSerializer( return ProductSimpleSerializer(
Product.objects.filter(uuid__in=obj.recently_viewed, is_active=True), many=True Product.objects.filter(uuid__in=obj.recently_viewed, is_active=True),
many=True,
).data ).data
class TokenObtainSerializer(Serializer): class TokenObtainSerializer(Serializer):
username_field = User.USERNAME_FIELD username_field = User.USERNAME_FIELD
token_class: Optional[Type[Token]] = None token_class: type[Token] | None = None
default_error_messages = {"no_active_account": _("no active account")} default_error_messages = {"no_active_account": _("no active account")}
@ -114,7 +116,7 @@ class TokenObtainSerializer(Serializer):
self.fields[self.username_field] = CharField(write_only=True) self.fields[self.username_field] = CharField(write_only=True)
self.fields["password"] = PasswordField() self.fields["password"] = PasswordField()
def validate(self, attrs: Dict[str, Any]) -> Dict[Any, Any]: def validate(self, attrs: dict[str, Any]) -> dict[Any, Any]:
authenticate_kwargs = { authenticate_kwargs = {
self.username_field: attrs[self.username_field], self.username_field: attrs[self.username_field],
"password": attrs["password"], "password": attrs["password"],
@ -140,7 +142,7 @@ class TokenObtainSerializer(Serializer):
class TokenObtainPairSerializer(TokenObtainSerializer): class TokenObtainPairSerializer(TokenObtainSerializer):
token_class = RefreshToken token_class = RefreshToken
def validate(self, attrs: Dict[str, Any]) -> Dict[str, str]: def validate(self, attrs: dict[str, Any]) -> dict[str, str]:
data = super().validate(attrs) data = super().validate(attrs)
logger.debug("Data validated") logger.debug("Data validated")
@ -165,7 +167,7 @@ class TokenRefreshSerializer(Serializer):
access = CharField(read_only=True) access = CharField(read_only=True)
token_class = RefreshToken token_class = RefreshToken
def validate(self, attrs: Dict[str, Any]) -> Dict[str, str]: def validate(self, attrs: dict[str, Any]) -> dict[str, str]:
refresh = self.token_class(attrs["refresh"]) refresh = self.token_class(attrs["refresh"])
data = {"access": str(refresh.access_token)} data = {"access": str(refresh.access_token)}
@ -189,7 +191,7 @@ class TokenRefreshSerializer(Serializer):
class TokenVerifySerializer(Serializer): class TokenVerifySerializer(Serializer):
token = CharField(write_only=True) token = CharField(write_only=True)
def validate(self, attrs: Dict[str, None]) -> Dict[Any, Any]: def validate(self, attrs: dict[str, None]) -> dict[Any, Any]:
token = UntypedToken(attrs["token"]) token = UntypedToken(attrs["token"])
if ( if (

View file

@ -46,9 +46,7 @@ class AuthTests(TestCase):
def test_obtain_token_view(self): def test_obtain_token_view(self):
url = reverse("token_obtain_pair") url = reverse("token_obtain_pair")
response = self.api_client.post( response = self.api_client.post(url, {"email": self.user.email, "password": "testpassword"})
url, {"email": self.user.email, "password": "testpassword"}
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn("access", response.data) self.assertIn("access", response.data)
self.assertIn("refresh", response.data) self.assertIn("refresh", response.data)
@ -58,9 +56,7 @@ class AuthTests(TestCase):
refresh_url = reverse("token_refresh") refresh_url = reverse("token_refresh")
# Obtain tokens # Obtain tokens
obtain_response = self.api_client.post( obtain_response = self.api_client.post(obtain_url, {"email": self.user.email, "password": "testpassword"})
obtain_url, {"email": self.user.email, "password": "testpassword"}
)
refresh_token = obtain_response.data["refresh"] refresh_token = obtain_response.data["refresh"]
# Refresh tokens # Refresh tokens
@ -73,9 +69,7 @@ class AuthTests(TestCase):
verify_url = reverse("token_verify") verify_url = reverse("token_verify")
# Obtain tokens # Obtain tokens
obtain_response = self.api_client.post( obtain_response = self.api_client.post(obtain_url, {"email": self.user.email, "password": "testpassword"})
obtain_url, {"email": self.user.email, "password": "testpassword"}
)
access_token = obtain_response.data["access"] access_token = obtain_response.data["access"]
# Verify token # Verify token