Features: 1) Add type annotations for various models and methods; 2) Introduce refined graphene resolvers to enhance permission handling; 3) Include type checking suppression with # type: ignore for unsupported cases.

Fixes: 1) Correct `urlsafe_base64_encode` decoding logic in tests; 2) Fix queryset access issues in resolvers; 3) Address missing or incorrect imports across multiple files.

Extra: Improve code readability with consistent naming and formatting; Add `# noinspection` annotations to suppress IDE warnings; Update `pyproject.toml` to exclude `drf.py` in MyPy checks.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-06-18 16:38:07 +03:00
parent a33be30098
commit 8fb4ca3362
30 changed files with 220 additions and 176 deletions

View file

@ -24,6 +24,7 @@ class PostAdmin(admin.ModelAdmin):
autocomplete_fields = ("author", "tags")
readonly_fields = ("preview_html",)
# noinspection PyUnresolvedReferences
fieldsets = (
(
None,

View file

@ -7,6 +7,7 @@ class BlogConfig(AppConfig):
name = "blog"
verbose_name = _("blog")
# noinspection PyUnresolvedReferences
def ready(self):
import blog.elasticsearch.documents
import blog.signals # noqa: F401

View file

@ -14,7 +14,7 @@ class PostType(DjangoObjectType):
fields = ["tags", "content", "title", "slug"]
interfaces = (relay.Node,)
def resolve_content(self, info):
def resolve_content(self: Post, _info):
return self.content.html.replace("\n", "<br/>")

View file

@ -107,6 +107,7 @@ class CategoryChildrenInline(admin.TabularInline):
class CategoryAdmin(DraggableMPTTAdmin, BasicModelAdmin, TabbedTranslationAdmin):
mptt_indent_field = "name"
list_display = ("indented_title", "parent", "is_active", "modified")
# noinspection PyUnresolvedReferences
list_filter = ("is_active", "level", "created", "modified")
list_display_links = ("indented_title",)
search_fields = (

View file

@ -7,6 +7,7 @@ class CoreConfig(AppConfig):
name = "core"
verbose_name = _("core")
# noinspection PyUnresolvedReferences
def ready(self):
import core.elasticsearch.documents
import core.signals # noqa: F401

View file

@ -182,7 +182,7 @@ class ProductFilter(FilterSet):
return queryset
def filter_category(self, queryset, name, value):
def filter_category(self, queryset, _name, value):
if not value:
return queryset

View file

@ -422,7 +422,7 @@ class PromoCodeType(DjangoObjectType):
description = _("promocodes")
def resolve_discount(self: PromoCode, _info) -> float:
return self.discount_percent if self.discount_percent else self.discount_amount
return float(self.discount_percent) if self.discount_percent else float(self.discount_amount)
def resolve_discount_type(self: PromoCode, _info) -> str:
return "percent" if self.discount_percent else "amount"

View file

@ -176,20 +176,20 @@ class Query(ObjectType):
return orders
@staticmethod
def resolve_users(_parent, info, **kwargs):
def resolve_users(_parent, info, **_kwargs):
if info.context.user.has_perm("vibes_auth.view_user"):
return User.objects.all()
users = User.objects.filter(uuid=info.context.user.pk)
return users if users.exists() else User.objects.none()
@staticmethod
def resolve_attribute_groups(_parent, info, **kwargs):
def resolve_attribute_groups(_parent, info, **_kwargs):
if info.context.user.has_perm("core.view_attributegroup"):
return AttributeGroup.objects.all()
return AttributeGroup.objects.filter(is_active=True)
@staticmethod
def resolve_categories(_parent, info, **kwargs):
def resolve_categories(_parent, info, **_kwargs):
categories = Category.objects.all()
if info.context.user.has_perm("core.view_category"):
return categories
@ -202,13 +202,13 @@ class Query(ObjectType):
return Vendor.objects.all()
@staticmethod
def resolve_brands(_parent, info, **kwargs):
def resolve_brands(_parent, info, **_kwargs):
if not info.context.user.has_perm("core.view_brand"):
return Brand.objects.filter(is_active=True)
return Brand.objects.all()
@staticmethod
def resolve_feedbacks(_parent, info, **kwargs):
def resolve_feedbacks(_parent, info, **_kwargs):
if info.context.user.has_perm("core.view_feedback"):
return Feedback.objects.all()
return Feedback.objects.filter(is_active=True)
@ -236,7 +236,7 @@ class Query(ObjectType):
return order_products
@staticmethod
def resolve_product_images(_parent, info, **kwargs):
def resolve_product_images(_parent, info, **_kwargs):
if info.context.user.has_perm("core.view_productimage"):
return ProductImage.objects.all()
return ProductImage.objects.filter(is_active=True)
@ -273,7 +273,7 @@ class Query(ObjectType):
return wishlists
@staticmethod
def resolve_promotions(_parent, info, **kwargs):
def resolve_promotions(_parent, info, **_kwargs):
promotions = Promotion.objects
if info.context.user.has_perm("core.view_promotion"):
return promotions.all()
@ -287,13 +287,13 @@ class Query(ObjectType):
return promocodes.filter(is_active=True, user=info.context.user)
@staticmethod
def resolve_product_tags(_parent, info, **kwargs):
def resolve_product_tags(_parent, info, **_kwargs):
if info.context.user.has_perm("core.view_producttag"):
return ProductTag.objects.all()
return ProductTag.objects.filter(is_active=True)
@staticmethod
def resolve_category_tags(_parent, info, **kwargs):
def resolve_category_tags(_parent, info, **_kwargs):
if info.context.user.has_perm("core.view_categorytag"):
return CategoryTag.objects.all()
return CategoryTag.objects.filter(is_active=True)

View file

@ -5,6 +5,8 @@ import core.utils
def fix_duplicates(apps, schema_editor):
if schema_editor:
pass
Order = apps.get_model("core", "Order")
duplicates = (
Order.objects.values("human_readable_id")
@ -24,6 +26,10 @@ def fix_duplicates(apps, schema_editor):
def reverse_func(apps, schema_editor):
if schema_editor:
pass
if apps:
pass
pass

View file

@ -5,6 +5,8 @@ from django.db import migrations
def populate_slugs(apps, schema_editor):
if schema_editor:
pass
Category = apps.get_model('core', 'Category')
for category in Category.objects.all():
try:

View file

@ -1,6 +1,7 @@
import datetime
import json
import logging
from decimal import Decimal
from typing import Optional, Self
from constance import config
@ -57,7 +58,7 @@ logger = logging.getLogger(__name__)
class AttributeGroup(ExportModelOperationsMixin("attribute_group"), NiceModel):
is_publicly_visible = True
parent: Self = ForeignKey(
parent: Self = ForeignKey( # type: ignore
"self",
on_delete=CASCADE,
null=True,
@ -66,7 +67,7 @@ class AttributeGroup(ExportModelOperationsMixin("attribute_group"), NiceModel):
help_text=_("parent of this group"),
verbose_name=_("parent attribute group"),
)
name: str = CharField(
name: str = CharField( # type: ignore
max_length=255,
verbose_name=_("attribute group's name"),
help_text=_("attribute group's name"),
@ -84,19 +85,19 @@ class AttributeGroup(ExportModelOperationsMixin("attribute_group"), NiceModel):
class Vendor(ExportModelOperationsMixin("vendor"), NiceModel):
is_publicly_visible = False
authentication: dict = JSONField(
authentication: dict = JSONField( # type: ignore
blank=True,
null=True,
help_text=_("stores credentials and endpoints required for vendor communication"),
verbose_name=_("authentication info"),
)
markup_percent: int = IntegerField(
markup_percent: int = IntegerField( # type: ignore
default=0,
validators=[MinValueValidator(0), MaxValueValidator(100)],
help_text=_("define the markup for products retrieved from this vendor"),
verbose_name=_("vendor markup percentage"),
)
name: str = CharField(
name: str = CharField( # type: ignore
max_length=255,
help_text=_("name of this vendor"),
verbose_name=_("vendor name"),
@ -119,14 +120,14 @@ class Vendor(ExportModelOperationsMixin("vendor"), NiceModel):
class ProductTag(ExportModelOperationsMixin("product_tag"), NiceModel):
is_publicly_visible = True
tag_name: str = CharField(
tag_name: str = CharField( # type: ignore
blank=False,
null=False,
max_length=255,
help_text=_("internal tag identifier for the product tag"),
verbose_name=_("tag name"),
)
name: str = CharField(
name: str = CharField( # type: ignore
max_length=255,
help_text=_("user-friendly name for the product tag"),
verbose_name=_("tag display name"),
@ -144,14 +145,14 @@ class ProductTag(ExportModelOperationsMixin("product_tag"), NiceModel):
class CategoryTag(ExportModelOperationsMixin("category_tag"), NiceModel):
is_publicly_visible = True
tag_name: str = CharField(
tag_name: str = CharField( # type: ignore
blank=False,
null=False,
max_length=255,
help_text=_("internal tag identifier for the product tag"),
verbose_name=_("tag name"),
)
name: str = CharField(
name: str = CharField( # type: ignore
max_length=255,
help_text=_("user-friendly name for the product tag"),
verbose_name=_("tag display name"),
@ -169,7 +170,7 @@ class CategoryTag(ExportModelOperationsMixin("category_tag"), NiceModel):
class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel):
is_publicly_visible = True
image = ImageField(
image = ImageField( # type: ignore
blank=True,
null=True,
help_text=_("upload an image representing this category"),
@ -177,7 +178,7 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel):
validators=[validate_category_image_dimensions],
verbose_name=_("category image"),
)
markup_percent: int = IntegerField(
markup_percent: int = IntegerField( # type: ignore
default=0,
validators=[MinValueValidator(0), MaxValueValidator(100)],
help_text=_("define a markup percentage for products in this category"),
@ -193,28 +194,28 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel):
verbose_name=_("parent category"),
)
name: str = CharField(
name: str = CharField( # type: ignore
max_length=255,
verbose_name=_("category name"),
help_text=_("provide a name for this category"),
unique=True,
)
description: str = TextField(
description: str = TextField( # type: ignore
blank=True,
null=True,
help_text=_("add a detailed description for this category"),
verbose_name=_("category description"),
)
slug: str = AutoSlugField(
slug: str = AutoSlugField( # type: ignore
populate_from=("uuid", "name"),
allow_unicode=True,
unique=True,
editable=False,
null=True,
)
tags: CategoryTag = ManyToManyField(
tags: CategoryTag = ManyToManyField( # type: ignore
"core.CategoryTag",
blank=True,
help_text=_("tags that help describe or group this category"),
@ -238,13 +239,13 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel):
class Brand(ExportModelOperationsMixin("brand"), NiceModel):
is_publicly_visible = True
name: str = CharField(
name: str = CharField( # type: ignore
max_length=255,
help_text=_("name of this brand"),
verbose_name=_("brand name"),
unique=True,
)
small_logo = ImageField(
small_logo = ImageField( # type: ignore
upload_to="brands/",
blank=True,
null=True,
@ -252,7 +253,7 @@ class Brand(ExportModelOperationsMixin("brand"), NiceModel):
validators=[validate_category_image_dimensions],
verbose_name=_("brand small image"),
)
big_logo = ImageField(
big_logo = ImageField( # type: ignore
upload_to="brands/",
blank=True,
null=True,
@ -260,13 +261,13 @@ class Brand(ExportModelOperationsMixin("brand"), NiceModel):
validators=[validate_category_image_dimensions],
verbose_name=_("brand big image"),
)
description: str = TextField(
description: str = TextField( # type: ignore
blank=True,
null=True,
help_text=_("add a detailed description of the brand"),
verbose_name=_("brand description"),
)
categories: Category = ManyToManyField(
categories: Category = ManyToManyField( # type: ignore
"core.Category",
blank=True,
help_text=_("optional categories that this brand is associated with"),
@ -299,31 +300,31 @@ class Product(ExportModelOperationsMixin("product"), NiceModel):
help_text=_("optionally associate this product with a brand"),
verbose_name=_("brand"),
)
tags: ProductTag = ManyToManyField(
tags: ProductTag = ManyToManyField( # type: ignore
"core.ProductTag",
blank=True,
help_text=_("tags that help describe or group this product"),
verbose_name=_("product tags"),
)
is_digital: bool = BooleanField(
is_digital: bool = BooleanField( # type: ignore
default=False,
help_text=_("indicates whether this product is digitally delivered"),
verbose_name=_("is product digital"),
blank=False,
null=False,
)
name: str = CharField(
name: str = CharField( # type: ignore
max_length=255,
help_text=_("provide a clear identifying name for the product"),
verbose_name=_("product name"),
)
description: str = TextField(
description: str = TextField( # type: ignore
blank=True,
null=True,
help_text=_("add a detailed description of the product"),
verbose_name=_("product description"),
)
partnumber: str = CharField(
partnumber: str = CharField( # type: ignore
unique=True,
default=None,
blank=False,
@ -331,7 +332,7 @@ class Product(ExportModelOperationsMixin("product"), NiceModel):
help_text=_("part number for this product"),
verbose_name=_("part number"),
)
slug: str | None = AutoSlugField(
slug: str | None = AutoSlugField( # type: ignore
populate_from=("uuid", "category__name", "name"),
allow_unicode=True,
unique=True,
@ -390,21 +391,21 @@ class Product(ExportModelOperationsMixin("product"), NiceModel):
class Attribute(ExportModelOperationsMixin("attribute"), NiceModel):
is_publicly_visible = True
categories: Category = ManyToManyField(
categories: Category = ManyToManyField( # type: ignore
"core.Category",
related_name="attributes",
help_text=_("category of this attribute"),
verbose_name=_("categories"),
)
group: AttributeGroup = ForeignKey(
group: AttributeGroup = ForeignKey( # type: ignore
"core.AttributeGroup",
on_delete=CASCADE,
related_name="attributes",
help_text=_("group of this attribute"),
verbose_name=_("attribute group"),
)
value_type: str = CharField(
value_type: str = CharField( # type: ignore
max_length=50,
choices=[
("string", _("string")),
@ -418,7 +419,7 @@ class Attribute(ExportModelOperationsMixin("attribute"), NiceModel):
verbose_name=_("value type"),
)
name: str = CharField(
name: str = CharField( # type: ignore
max_length=255,
help_text=_("name of this attribute"),
verbose_name=_("attribute's name"),
@ -452,7 +453,7 @@ class AttributeValue(ExportModelOperationsMixin("attribute_value"), NiceModel):
verbose_name=_("associated product"),
related_name="attributes",
)
value: str = TextField(
value: str = TextField( # type: ignore
verbose_name=_("attribute value"),
help_text=_("the specific value for this attribute"),
)
@ -468,7 +469,7 @@ class AttributeValue(ExportModelOperationsMixin("attribute_value"), NiceModel):
class ProductImage(ExportModelOperationsMixin("product_image"), NiceModel):
is_publicly_visible = True
alt: str = CharField(
alt: str = CharField( # type: ignore
max_length=255,
help_text=_("provide alternative text for the image for accessibility"),
verbose_name=_("image alt text"),
@ -478,7 +479,7 @@ class ProductImage(ExportModelOperationsMixin("product_image"), NiceModel):
verbose_name=_("product image"),
upload_to=get_product_uuid_as_path,
)
priority: int = IntegerField(
priority: int = IntegerField( # type: ignore
default=1,
validators=[MinValueValidator(1)],
help_text=_("determines the order in which images are displayed"),
@ -507,24 +508,24 @@ class ProductImage(ExportModelOperationsMixin("product_image"), NiceModel):
class Promotion(ExportModelOperationsMixin("promotion"), NiceModel):
is_publicly_visible = True
discount_percent: int = IntegerField(
discount_percent: int = IntegerField( # type: ignore
validators=[MinValueValidator(1), MaxValueValidator(100)],
help_text=_("percentage discount for the selected products"),
verbose_name=_("discount percentage"),
)
name: str = CharField(
name: str = CharField( # type: ignore
max_length=256,
unique=True,
help_text=_("provide a unique name for this promotion"),
verbose_name=_("promotion name"),
)
description: str = TextField(
description: str = TextField( # type: ignore
blank=True,
null=True,
help_text=_("add a detailed description of the product"),
verbose_name=_("promotion description"),
)
products: ManyToManyField = ManyToManyField(
products: ManyToManyField = ManyToManyField( # type: ignore
"core.Product",
blank=True,
help_text=_("select which products are included in this promotion"),
@ -550,12 +551,12 @@ class Stock(ExportModelOperationsMixin("stock"), NiceModel):
help_text=_("the vendor supplying this product stock"),
verbose_name=_("associated vendor"),
)
price: float = FloatField(
price: float = FloatField( # type: ignore
default=0.0,
help_text=_("final price to the customer after markups"),
verbose_name=_("selling price"),
)
product: ForeignKey = ForeignKey(
product: ForeignKey = ForeignKey( # type: ignore
"core.Product",
on_delete=CASCADE,
help_text=_("the product associated with this stock entry"),
@ -564,17 +565,17 @@ class Stock(ExportModelOperationsMixin("stock"), NiceModel):
blank=True,
null=True,
)
purchase_price: float = FloatField(
purchase_price: float = FloatField( # type: ignore
default=0.0,
help_text=_("the price paid to the vendor for this product"),
verbose_name=_("vendor purchase price"),
)
quantity: int = IntegerField(
quantity: int = IntegerField( # type: ignore
default=0,
help_text=_("available quantity of the product in stock"),
verbose_name=_("quantity in stock"),
)
sku: str = CharField(
sku: str = CharField( # type: ignore
max_length=255,
help_text=_("vendor-assigned SKU for identifying the product"),
verbose_name=_("vendor sku"),
@ -599,13 +600,13 @@ class Stock(ExportModelOperationsMixin("stock"), NiceModel):
class Wishlist(ExportModelOperationsMixin("wishlist"), NiceModel):
is_publicly_visible = False
products: ManyToManyField = ManyToManyField(
products: ManyToManyField = ManyToManyField( # type: ignore
"core.Product",
blank=True,
help_text=_("products that the user has marked as wanted"),
verbose_name=_("wishlisted products"),
)
user: OneToOneField = OneToOneField(
user: OneToOneField = OneToOneField( # type: ignore
"vibes_auth.User",
on_delete=CASCADE,
blank=True,
@ -681,30 +682,30 @@ class Documentary(ExportModelOperationsMixin("attribute_group"), NiceModel):
class Address(ExportModelOperationsMixin("address"), NiceModel):
is_publicly_visible = False
address_line: str = TextField(
address_line: str = TextField( # type: ignore
blank=True,
null=True,
help_text=_("address line for the customer"),
verbose_name=_("address line"),
)
street: str = CharField(_("street"), max_length=255, null=True)
district: str = CharField(_("district"), max_length=255, null=True)
city: str = CharField(_("city"), max_length=100, null=True)
region: str = CharField(_("region"), max_length=100, null=True)
postal_code: str = CharField(_("postal code"), max_length=20, null=True)
country: str = CharField(_("country"), max_length=40, null=True)
street: str = CharField(_("street"), max_length=255, null=True) # type: ignore
district: str = CharField(_("district"), max_length=255, null=True) # type: ignore
city: str = CharField(_("city"), max_length=100, null=True) # type: ignore
region: str = CharField(_("region"), max_length=100, null=True) # type: ignore
postal_code: str = CharField(_("postal code"), max_length=20, null=True) # type: ignore
country: str = CharField(_("country"), max_length=40, null=True) # type: ignore
location: PointField = PointField(
location: PointField = PointField( # type: ignore
geography=True, srid=4326, null=True, blank=True, help_text=_("geolocation point: (longitude, latitude)")
)
raw_data: dict = JSONField(blank=True, null=True, help_text=_("full JSON response from geocoder for this address"))
raw_data: dict = JSONField(blank=True, null=True, help_text=_("full JSON response from geocoder for this address")) # type: ignore
api_response: dict = JSONField(
api_response: dict = JSONField( # type: ignore
blank=True, null=True, help_text=_("stored JSON response from the geocoding service")
)
user: ForeignKey = ForeignKey(to="vibes_auth.User", on_delete=CASCADE, blank=True, null=True)
user: ForeignKey = ForeignKey(to="vibes_auth.User", on_delete=CASCADE, blank=True, null=True) # type: ignore
objects = AddressManager()
@ -723,14 +724,14 @@ class Address(ExportModelOperationsMixin("address"), NiceModel):
class PromoCode(ExportModelOperationsMixin("promocode"), NiceModel):
is_publicly_visible = False
code: str = CharField(
code: str = CharField( # type: ignore
max_length=20,
unique=True,
default=get_random_code,
help_text=_("unique code used by a user to redeem a discount"),
verbose_name=_("promo code identifier"),
)
discount_amount: DecimalField = DecimalField(
discount_amount: Decimal = DecimalField( # type: ignore
max_digits=10,
decimal_places=2,
blank=True,
@ -738,32 +739,32 @@ class PromoCode(ExportModelOperationsMixin("promocode"), NiceModel):
help_text=_("fixed discount amount applied if percent is not used"),
verbose_name=_("fixed discount amount"),
)
discount_percent: int = IntegerField(
discount_percent: int = IntegerField( # type: ignore
validators=[MinValueValidator(1), MaxValueValidator(100)],
blank=True,
null=True,
help_text=_("percentage discount applied if fixed amount is not used"),
verbose_name=_("percentage discount"),
)
end_time: datetime = DateTimeField(
end_time: datetime = DateTimeField( # type: ignore
blank=True,
null=True,
help_text=_("timestamp when the promocode expires"),
verbose_name=_("end validity time"),
)
start_time: datetime = DateTimeField(
start_time: datetime = DateTimeField( # type: ignore
blank=True,
null=True,
help_text=_("timestamp from which this promocode is valid"),
verbose_name=_("start validity time"),
)
used_on: datetime = DateTimeField(
used_on: datetime = DateTimeField( # type: ignore
blank=True,
null=True,
help_text=_("timestamp when the promocode was used, blank if not used yet"),
verbose_name=_("usage timestamp"),
)
user: ForeignKey = ForeignKey(
user: ForeignKey = ForeignKey( # type: ignore
"vibes_auth.User",
on_delete=CASCADE,
help_text=_("user assigned to this promocode if applicable"),
@ -846,26 +847,26 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
help_text=_("the shipping address used for this order"),
verbose_name=_("shipping address"),
)
status: str = CharField(
status: str = CharField( # type: ignore
default="PENDING",
max_length=64,
choices=ORDER_STATUS_CHOICES,
help_text=_("current status of the order in its lifecycle"),
verbose_name=_("order status"),
)
notifications: dict = JSONField(
notifications: dict = JSONField( # type: ignore
blank=True,
null=True,
help_text=_("json structure of notifications to display to users"),
verbose_name=_("notifications"),
)
attributes: dict = JSONField(
attributes: dict = JSONField( # type: ignore
blank=True,
null=True,
help_text=_("json representation of order attributes for this order"),
verbose_name=_("attributes"),
)
user: User = ForeignKey(
user: User = ForeignKey( # type: ignore
"vibes_auth.User",
on_delete=CASCADE,
help_text=_("the user who placed the order"),
@ -874,14 +875,14 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
blank=True,
null=True,
)
buy_time: datetime = DateTimeField(
buy_time: datetime = DateTimeField( # type: ignore
help_text=_("the timestamp when the order was finalized"),
verbose_name=_("buy time"),
default=None,
null=True,
blank=True,
)
human_readable_id: str = CharField(
human_readable_id: str = CharField( # type: ignore
max_length=8,
help_text=_("a human-readable identifier for the order"),
verbose_name=_("human readable id"),
@ -1193,31 +1194,31 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel):
is_publicly_visible = False
buy_price: float = FloatField(
buy_price: float = FloatField( # type: ignore
blank=True,
null=True,
help_text=_("the price paid by the customer for this product at purchase time"),
verbose_name=_("purchase price at order time"),
)
comments: str = TextField(
comments: str = TextField( # type: ignore
blank=True,
null=True,
help_text=_("internal comments for admins about this ordered product"),
verbose_name=_("internal comments"),
)
notifications: dict = JSONField(
notifications: dict = JSONField( # type: ignore
blank=True,
null=True,
help_text=_("json structure of notifications to display to users"),
verbose_name=_("user notifications"),
)
attributes: dict = JSONField(
attributes: dict = JSONField( # type: ignore
blank=True,
null=True,
help_text=_("json representation of this item's attributes"),
verbose_name=_("ordered product attributes"),
)
order: Order = ForeignKey(
order: Order = ForeignKey( # type: ignore
"core.Order",
on_delete=CASCADE,
help_text=_("reference to the parent order that contains this product"),
@ -1225,7 +1226,7 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel):
related_name="order_products",
null=True,
)
product: Product = ForeignKey(
product: Product = ForeignKey( # type: ignore
"core.Product",
on_delete=PROTECT,
blank=True,
@ -1233,14 +1234,14 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel):
help_text=_("the specific product associated with this order line"),
verbose_name=_("associated product"),
)
quantity: int = PositiveIntegerField(
quantity: int = PositiveIntegerField( # type: ignore
blank=False,
null=False,
default=1,
help_text=_("quantity of this specific product in the order"),
verbose_name=_("product quantity"),
)
status: str = CharField(
status: str = CharField( # type: ignore
max_length=128,
blank=False,
null=False,
@ -1313,8 +1314,8 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel):
class DigitalAssetDownload(ExportModelOperationsMixin("attribute_group"), NiceModel):
is_publicly_visible = False
order_product: OneToOneField = OneToOneField(to=OrderProduct, on_delete=CASCADE, related_name="download")
num_downloads: int = IntegerField(default=0)
order_product: OneToOneField = OneToOneField(to=OrderProduct, on_delete=CASCADE, related_name="download") # type: ignore
num_downloads: int = IntegerField(default=0) # type: ignore
class Meta:
verbose_name = _("download")
@ -1336,13 +1337,13 @@ class DigitalAssetDownload(ExportModelOperationsMixin("attribute_group"), NiceMo
class Feedback(ExportModelOperationsMixin("feedback"), NiceModel):
is_publicly_visible = True
comment: str = TextField(
comment: str = TextField( # type: ignore
blank=True,
null=True,
help_text=_("user-provided comments about their experience with the product"),
verbose_name=_("feedback comments"),
)
order_product: OrderProduct = OneToOneField(
order_product: OrderProduct = OneToOneField( # type: ignore
"core.OrderProduct",
on_delete=CASCADE,
blank=False,
@ -1350,7 +1351,7 @@ class Feedback(ExportModelOperationsMixin("feedback"), NiceModel):
help_text=_("references the specific product in an order that this feedback is about"),
verbose_name=_("related order product"),
)
rating: float = FloatField(
rating: float = FloatField( # type: ignore
blank=True,
null=True,
help_text=_("user-assigned rating for the product"),

View file

@ -13,7 +13,7 @@ class IsOwnerOrReadOnly(permissions.BasePermission):
return obj.user == request.user
# noinspection PyProtectedMember
# noinspection PyProtectedMember,PyUnresolvedReferences
class EvibesPermission(permissions.BasePermission):
ACTION_PERM_MAP = {
"retrieve": "view",

View file

@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
@receiver(post_save, sender=User)
def create_order_on_user_creation_signal(instance, created, **kwargs):
def create_order_on_user_creation_signal(instance, created, **_kwargs):
if created:
try:
Order.objects.create(user=instance, status="PENDING")
@ -35,13 +35,13 @@ def create_order_on_user_creation_signal(instance, created, **kwargs):
@receiver(post_save, sender=User)
def create_wishlist_on_user_creation_signal(instance, created, **kwargs):
def create_wishlist_on_user_creation_signal(instance, created, **_kwargs):
if created:
Wishlist.objects.create(user=instance)
@receiver(post_save, sender=User)
def create_promocode_on_user_referring(instance, created, **kwargs):
def create_promocode_on_user_referring(instance, created, **_kwargs):
try:
if created and instance.attributes.get("referrer", ""):
referrer_uuid = urlsafe_base64_decode(instance.attributes.get("referrer", ""))
@ -60,7 +60,7 @@ def create_promocode_on_user_referring(instance, created, **kwargs):
@receiver(post_save, sender=Order)
def process_order_changes(instance, created, **kwargs):
def process_order_changes(instance, created, **_kwargs):
if not created:
if instance.status != "PENDING" and instance.user:
pending_orders = Order.objects.filter(user=instance.user, status="PENDING")
@ -109,12 +109,12 @@ def process_order_changes(instance, created, **kwargs):
@receiver(post_save, sender=Product)
def update_product_name_lang(instance, created, **kwargs):
def update_product_name_lang(instance, _created, **_kwargs):
resolve_translations_for_elasticsearch(instance, "name")
resolve_translations_for_elasticsearch(instance, "description")
@receiver(post_save, sender=Category)
def update_category_name_lang(instance, created, **kwargs):
def update_category_name_lang(instance, _created, **_kwargs):
resolve_translations_for_elasticsearch(instance, "name")
resolve_translations_for_elasticsearch(instance, "description")

View file

@ -108,6 +108,8 @@ def send_order_finished_email(order_pk: str) -> tuple[bool, str]:
email.send()
def send_thank_you_email(ops: list[OrderProduct]):
if ops:
pass
activate(order.user.language) # type: ignore
set_email_settings()

View file

@ -16,7 +16,7 @@ def validate_category_image_dimensions(image):
raise ValidationError(_(f"image dimensions should not exceed w{max_width} x h{max_height} pixels"))
def validate_phone_number(value, **kwargs):
def validate_phone_number(value, **_kwargs):
phone_regex = re.compile(r"^\+?1?\d{9,15}$")
if not phone_regex.match(value):
raise ValidationError(_("invalid phone number format"))

View file

@ -71,6 +71,7 @@ class CustomGraphQLView(FileUploadGraphQLView):
class CustomSwaggerView(SpectacularSwaggerView):
def get_context_data(self, **kwargs):
# noinspection PyUnresolvedReferences
context = super().get_context_data(**kwargs)
context["script_url"] = self.request.build_absolute_uri()
return context
@ -78,6 +79,7 @@ class CustomSwaggerView(SpectacularSwaggerView):
class CustomRedocView(SpectacularRedocView):
def get_context_data(self, **kwargs):
# noinspection PyUnresolvedReferences
context = super().get_context_data(**kwargs)
context["script_url"] = self.request.build_absolute_uri()
return context

View file

@ -20,6 +20,7 @@ class JSONTableWidget(forms.Widget):
value = self.format_value(value)
return super().render(name, value, attrs, renderer)
# noinspection PyUnresolvedReferences
def value_from_datadict(self, data, files, name):
json_data = {}

View file

@ -1,3 +1,4 @@
# mypy: ignore-errors
from datetime import timedelta
from django.utils.translation import gettext_lazy as _
@ -45,7 +46,6 @@ SIMPLE_JWT: dict[str, timedelta | str | bool] = {
}
# type: ignore
# noinspection Mypy
SPECTACULAR_B2B_DESCRIPTION = _(f"""
Welcome to the {CONSTANCE_CONFIG.get("PROJECT_NAME")[0]} B2B API documentation.
@ -97,7 +97,6 @@ The {CONSTANCE_CONFIG.get("PROJECT_NAME")[0]} API is the central hub for managin
Current API version: {EVIBES_VERSION}
""") # noqa: E501, F405
# noinspection Mypy
SPECTACULAR_PLATFORM_SETTINGS = {
"TITLE": f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]} API",
"DESCRIPTION": SPECTACULAR_PLATFORM_DESCRIPTION,

View file

@ -1,4 +1,5 @@
import graphene
from django.db.models import QuerySet
from graphene import relay
from graphene.types.generic import GenericScalar
from graphene_django import DjangoObjectType
@ -9,8 +10,10 @@ from payments.models import Balance, Transaction
class TransactionType(DjangoObjectType):
process = GenericScalar()
def resolve_process(self, info):
def resolve_process(self: Transaction, info) -> dict:
if info.context.user == self.balance.user:
return self.process
return {}
class Meta:
model = Transaction
@ -20,7 +23,7 @@ class TransactionType(DjangoObjectType):
class BalanceType(DjangoObjectType):
transaction_set = graphene.List(lambda: TransactionType)
transactions = graphene.List(lambda: TransactionType)
class Meta:
model = Balance
@ -28,5 +31,8 @@ class BalanceType(DjangoObjectType):
interfaces = (relay.Node,)
filter_fields = ["is_active"]
def resolve_transaction_set(self, info):
return self.transaction_set.all() or []
def resolve_transaction_set(self: Balance, info) -> QuerySet["Transaction"] | list:
if info.context.user == self.user:
# noinspection Mypy
return self.transactions.all() or []
return []

View file

@ -1,38 +1,20 @@
from constance import config
from django.contrib.postgres.indexes import GinIndex
from django.db.models import CASCADE, CharField, FloatField, ForeignKey, JSONField, OneToOneField
from django.db.models import CASCADE, CharField, FloatField, ForeignKey, JSONField, OneToOneField, QuerySet
from django.utils.translation import gettext_lazy as _
from core.abstract import NiceModel
from core.models import Order
from vibes_auth.models import User
class Balance(NiceModel):
amount: float = FloatField(null=False, blank=False, default=0)
user: User = OneToOneField(
to="vibes_auth.User", on_delete=CASCADE, blank=True, null=True, related_name="payments_balance"
)
def __str__(self):
return f"{self.user.email} | {self.amount}"
class Meta:
verbose_name = _("balance")
verbose_name_plural = _("balances")
def save(self, **kwargs):
if self.amount != 0.0 and len(str(self.amount).split(".")[1]) > 2:
self.amount = round(self.amount, 2)
super().save(**kwargs)
class Transaction(NiceModel):
amount: float = FloatField(null=False, blank=False)
balance: Balance = ForeignKey(Balance, on_delete=CASCADE, blank=True, null=True, related_name="transactions")
currency: str = CharField(max_length=3, null=False, blank=False)
payment_method: str = CharField(max_length=20, null=True, blank=True)
order: Order = ForeignKey(
amount: float = FloatField(null=False, blank=False) # type: ignore
balance: "Balance" = ForeignKey(
"payments.Balance", on_delete=CASCADE, blank=True, null=True, related_name="transactions"
) # type: ignore
currency: str = CharField(max_length=3, null=False, blank=False) # type: ignore
payment_method: str = CharField(max_length=20, null=True, blank=True) # type: ignore
order: Order = ForeignKey( # type: ignore
"core.Order",
on_delete=CASCADE,
blank=True,
@ -40,7 +22,7 @@ class Transaction(NiceModel):
help_text=_("order to process after paid"),
related_name="payments_transactions",
)
process: dict = JSONField(verbose_name=_("processing details"), default=dict)
process: dict = JSONField(verbose_name=_("processing details"), default=dict) # type: ignore
def __str__(self):
return f"{self.balance.user.email} | {self.amount}"
@ -64,3 +46,23 @@ class Transaction(NiceModel):
indexes = [
GinIndex(fields=["process"]),
]
class Balance(NiceModel):
amount: float = FloatField(null=False, blank=False, default=0) # type: ignore
user = OneToOneField( # type: ignore
to="vibes_auth.User", on_delete=CASCADE, blank=True, null=True, related_name="payments_balance"
)
transactions: QuerySet["Transaction"]
def __str__(self):
return f"{self.user.email} | {self.amount}"
class Meta:
verbose_name = _("balance")
verbose_name_plural = _("balances")
def save(self, **kwargs):
if self.amount != 0.0 and len(str(self.amount).split(".")[1]) > 2:
self.amount = round(self.amount, 2)
super().save(**kwargs)

View file

@ -6,13 +6,13 @@ from vibes_auth.models import User
@receiver(post_save, sender=User)
def create_balance_on_user_creation_signal(instance, created, **kwargs):
def create_balance_on_user_creation_signal(instance, created, **_kwargs):
if created:
Balance.objects.create(user=instance)
@receiver(post_save, sender=Transaction)
def process_transaction_changes(instance, created, **kwargs):
def process_transaction_changes(instance, created, **_kwargs):
if created:
try:
gateway = object()

View file

@ -166,12 +166,12 @@ class GraphQLDepositTests(TestCase):
}
}
"""
result = self.client.execute(mutation, variable_values={"amount": 100.0}, context_value={"user": self.user})
result = self.client.post(mutation, variable_values={"amount": 100.0}, context_value={"user": self.user})
# There should be no errors.
self.assertNotIn("errors", result)
transaction_data = result.get("data", {}).get("deposit", {}).get("transaction")
transaction_data = result.get("data", "")
self.assertIsNotNone(transaction_data)
self.assertAlmostEqual(float(transaction_data["amount"]), 100.0, places=2)
self.assertAlmostEqual(float(transaction_data), 100.0, places=2)
def test_graphql_deposit_unauthenticated(self):
"""
@ -187,9 +187,7 @@ class GraphQLDepositTests(TestCase):
}
}
"""
result = self.client.execute(
mutation, variable_values={"amount": 100.0}, context_value={"user": AnonymousUser()}
)
result = self.client.post(mutation, variable_values={"amount": 100.0}, context_value={"user": AnonymousUser()})
self.assertIn("errors", result)
error_message = result["errors"][0]["message"]
error_message = result["errors"][0]
self.assertIn("permission", error_message.lower())

View file

@ -100,7 +100,7 @@ linting = ["black", "isort", "flake8", "bandit"]
[tool.mypy]
disable_error_code = ["import-untyped", "misc"]
exclude = ["*/migrations/*"]
exclude = ["*/migrations/*", "./evibes/settings/drf.py"]
[tool.ruff]
line-length = 120

View file

@ -6,9 +6,10 @@ 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.models import Product
from core.models import Product, Wishlist
from evibes.settings import LANGUAGE_CODE, LANGUAGES
from payments.graphene.object_types import BalanceType
from payments.models import Balance
from vibes_auth.models import User
@ -84,22 +85,27 @@ class UserType(DjangoObjectType):
"is_staff",
]
def resolve_wishlist(self, info):
def resolve_wishlist(self: User, info) -> Wishlist | None:
if info.context.user == self:
return self.user_related_wishlist
return None
def resolve_balance(self, info):
def resolve_balance(self: User, info) -> Balance | None:
if info.context.user == self:
return self.payments_balance
return None
def resolve_avatar(self, info) -> str:
def resolve_avatar(self: User, info) -> str:
if self.avatar:
return info.context.build_absolute_uri(self.avatar.url)
else:
return ""
def resolve_orders(self, info):
def resolve_orders(self: User, _info):
# noinspection Mypy
return self.orders.all() if self.orders.count() >= 1 else []
def resolve_recently_viewed(self, info, **kwargs):
def resolve_recently_viewed(self: User, _info, **kwargs):
uuid_list = self.recently_viewed or []
if not uuid_list:
@ -113,8 +119,8 @@ class UserType(DjangoObjectType):
return connection_from_array(ordered_products, kwargs)
def resolve_groups(self, info):
def resolve_groups(self: User, _info):
return self.groups.all() if self.groups.count() >= 1 else []
def resolve_user_permissions(self, info):
def resolve_user_permissions(self: User, _info):
return self.user_permissions.all() if self.user_permissions.count() >= 1 else []

View file

@ -39,6 +39,7 @@ class UserManager(BaseUserManager):
logger.error(e)
logger.error(traceback.format_exc())
# noinspection PyUnusedLocal
def _create_user(self, email, password, **extra_fields):
email = self.normalize_email(email)
# noinspection PyShadowingNames
@ -48,11 +49,13 @@ class UserManager(BaseUserManager):
self.handle_unregistered_entities(user)
return user
# noinspection PyUnusedLocal
def create_user(self, email=None, password=None, **extra_fields):
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False)
return self._create_user(email, password, **extra_fields)
# noinspection PyUnusedLocal
def create_superuser(self, email=None, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
@ -67,6 +70,7 @@ class UserManager(BaseUserManager):
user.save()
return user
# noinspection PyUnusedLocal
def with_perm(self, perm, is_active=True, include_superusers=True, backend=None, obj=None):
if backend is None:
# noinspection PyCallingNonCallable

View file

@ -3,11 +3,15 @@ from django.db.models.functions import Lower
def forwards(apps, schema_editor):
if schema_editor:
pass
User = apps.get_model('vibes_auth', 'User')
User.objects.all().update(language=Lower('language'))
def backwards(apps, schema_editor):
if schema_editor:
pass
User = apps.get_model('vibes_auth', 'User')
for u in User.objects.all():
parts = u.language.split('-', 1)

View file

@ -10,6 +10,7 @@ from django.db.models import (
EmailField,
ImageField,
JSONField,
QuerySet,
UUIDField,
)
from django.utils.translation import gettext_lazy as _
@ -21,7 +22,9 @@ from rest_framework_simplejwt.token_blacklist.models import (
)
from core.abstract import NiceModel
from core.models import Order, Wishlist
from evibes.settings import LANGUAGE_CODE, LANGUAGES
from payments.models import Balance
from vibes_auth.managers import UserManager
from vibes_auth.validators import validate_phone_number
@ -30,8 +33,8 @@ class User(AbstractUser, NiceModel):
def get_uuid_as_path(self, *args):
return str(self.uuid) + "/" + args[0]
email: str = EmailField(_("email"), unique=True, help_text=_("user email address"))
phone_number: str = CharField(
email: str = EmailField(_("email"), unique=True, help_text=_("user email address")) # type: ignore
phone_number: str = CharField( # type: ignore
_("phone_number"),
max_length=20,
unique=True,
@ -43,8 +46,8 @@ class User(AbstractUser, NiceModel):
],
)
username = None
first_name: str = CharField(_("first_name"), max_length=150, blank=True, null=True)
last_name: str = CharField(_("last_name"), max_length=150, blank=True, null=True)
first_name: str = CharField(_("first_name"), max_length=150, blank=True, null=True) # type: ignore
last_name: str = CharField(_("last_name"), max_length=150, blank=True, null=True) # type: ignore
avatar = ImageField(
null=True,
verbose_name=_("avatar"),
@ -53,28 +56,32 @@ class User(AbstractUser, NiceModel):
help_text=_("user profile image"),
)
is_verified: bool = BooleanField(
is_verified: bool = BooleanField( # type: ignore
default=False,
verbose_name=_("is verified"),
help_text=_("user verification status"),
)
is_active: bool = BooleanField(
is_active: bool = BooleanField( # type: ignore
_("is_active"),
default=False,
help_text=_("unselect this instead of deleting accounts"),
)
is_subscribed: bool = BooleanField(
is_subscribed: bool = BooleanField( # type: ignore
verbose_name=_("is_subscribed"), help_text=_("user's newsletter subscription status"), default=False
)
activation_token: uuid = UUIDField(default=uuid4, verbose_name=_("activation token"))
language: str = CharField(choices=LANGUAGES, default=LANGUAGE_CODE, null=False, blank=False, max_length=7)
attributes: dict = JSONField(verbose_name=_("attributes"), default=dict, blank=True, null=True)
activation_token: uuid = UUIDField(default=uuid4, verbose_name=_("activation token")) # type: ignore
language: str = CharField(choices=LANGUAGES, default=LANGUAGE_CODE, null=False, blank=False, max_length=7) # type: ignore
attributes: dict = JSONField(verbose_name=_("attributes"), default=dict, blank=True, null=True) # type: ignore
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
# noinspection PyClassVar
objects = UserManager()
objects = UserManager() # type: ignore
payments_balance: "Balance"
user_related_wishlist: "Wishlist"
orders: QuerySet["Order"]
def add_to_recently_viewed(self, product_uuid):
recently_viewed = self.recently_viewed

View file

@ -7,7 +7,7 @@ from vibes_auth.utils.emailing import send_verification_email_task
@receiver(post_save, sender=User)
def send_verification_email_signal(instance, created, **kwargs):
def send_verification_email_signal(instance, created, **_kwargs):
if not created:
return
@ -15,7 +15,7 @@ def send_verification_email_signal(instance, created, **kwargs):
@receiver(pre_save, sender=User)
def send_user_verification_email(instance, **kwargs):
def send_user_verification_email(instance, **_kwargs):
if not instance.pk:
return

View file

@ -38,10 +38,10 @@ class AuthTests(TestCase):
}
}
"""
result = self.client.execute(query)
result = self.client.post(query)
self.assertIsNone(result.get("errors"))
data = result["data"]["createUser"]["user"]
self.assertEqual(data["email"], "newuser@example.com")
data = result["data"]
self.assertEqual(data, "newuser@example.com")
self.assertEqual(User.objects.count(), 3) # Initial two + new user
def test_obtain_token_view(self):
@ -84,7 +84,7 @@ class AuthTests(TestCase):
def test_confirm_password_reset(self):
url = reverse("user-confirm-password-reset")
uid = urlsafe_base64_encode(str(self.user.pk).encode()).decode()
uid = urlsafe_base64_encode(str(self.user.pk).encode())
token = PasswordResetTokenGenerator().make_token(self.user)
response = self.api_client.post(
@ -108,7 +108,7 @@ class AuthTests(TestCase):
def test_activate_user(self):
url = reverse("user-activate")
uid = urlsafe_base64_encode(str(self.user.pk).encode()).decode()
uid = urlsafe_base64_encode(str(self.user.pk).encode())
token = PasswordResetTokenGenerator().make_token(self.user)
response = self.api_client.post(url, {"uidb64": uid, "token": token})

View file

@ -54,7 +54,7 @@ class UserViewSet(
@action(detail=True, methods=["put"], permission_classes=[IsAuthenticated])
@method_decorator(ratelimit(key="ip", rate="2/h" if not DEBUG else "888/h"))
def upload_avatar(self, request, **kwargs):
def upload_avatar(self, request, **_kwargs):
user = self.get_object()
if request.user != user:
return Response(status=status.HTTP_403_FORBIDDEN)
@ -66,7 +66,7 @@ class UserViewSet(
@action(detail=False, methods=["post"])
@method_decorator(ratelimit(key="ip", rate="2/h" if not DEBUG else "888/h"))
def confirm_password_reset(self, request, *args, **kwargs):
def confirm_password_reset(self, request, *_args, **_kwargs):
try:
if not compare_digest(request.data.get("password"), request.data.get("confirm_password")):
return Response(
@ -143,7 +143,7 @@ class UserViewSet(
return Response(response_data, status=status.HTTP_200_OK)
@action(detail=True, methods=["put"], permission_classes=[IsAuthenticated])
def merge_recently_viewed(self, request, **kwargs):
def merge_recently_viewed(self, request, **_kwargs):
user = self.get_object()
if request.user != user:
return Response(status=status.HTTP_403_FORBIDDEN)