Features: 1) Added vendor, product tag, category tag models and metadata; 2) Introduced proper noinspection comments for Mypy warnings; 3) Extended Markdown linting rules.

Fixes: 1) Corrected `ForeignKey` type assertions across models; 2) Resolved typos and formatting inconsistencies in `.env` and README; 3) Fixed explicit boolean checks in user manager methods.

Extra: Updated type hints in multiple models, serializers, and views.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-06-18 15:58:49 +03:00
parent 328ccaa615
commit a33be30098
30 changed files with 716 additions and 690 deletions

1
.gitignore vendored
View file

@ -87,6 +87,7 @@ wheels/
share/python-wheels/ share/python-wheels/
pip-log.txt pip-log.txt
pip-delete-this-directory.txt pip-delete-this-directory.txt
desktop.ini
# Node build artifacts # Node build artifacts
npm-debug.log* npm-debug.log*

View file

@ -103,12 +103,12 @@ decomment previously commented lines and enjoy eVibes over HTTPS!
### .env ### .env
After .env file generation, you may want to edit some of its values, such as macroservices' API keys, database password, After .env file generation, you may want to edit some of its values, such as macroservices` API keys, database password,
redis password, etc. redis password, etc.
## Usage ## Usage
- Add necessary subdomains to DNS-settings of your domain, those are: - Add the necessary subdomains to DNS-settings of your domain, those are:
1. @.your-domain.com 1. @.your-domain.com
2. www.your-domain.com 2. www.your-domain.com

View file

@ -8,6 +8,13 @@ from .models import Post, PostTag
@admin.register(Post) @admin.register(Post)
class PostAdmin(admin.ModelAdmin): class PostAdmin(admin.ModelAdmin):
def preview_html(self, obj):
html = obj.content.html or "<em>{}</em>".format(_("(no content yet)"))
# noinspection DjangoSafeString
return mark_safe(html)
preview_html.short_description = _("rendered HTML") # type: ignore
form = PostAdminForm form = PostAdminForm
list_display = ("title", "author", "slug", "created", "modified") list_display = ("title", "author", "slug", "created", "modified")
list_filter = ("author", "tags", "created", "modified") list_filter = ("author", "tags", "created", "modified")
@ -33,13 +40,6 @@ class PostAdmin(admin.ModelAdmin):
), ),
) )
def preview_html(self, obj):
html = obj.content.html or "<em>{}</em>".format(_("(no content yet)"))
# noinspection DjangoSafeString
return mark_safe(html)
preview_html.short_description = _("rendered HTML") # type: ignore
@admin.register(PostTag) @admin.register(PostTag)
class PostTagAdmin(admin.ModelAdmin): class PostTagAdmin(admin.ModelAdmin):

View file

@ -1,3 +1,4 @@
# noinspection PyUnresolvedReferences
from constance.admin import Config from constance.admin import Config
from constance.admin import ConstanceAdmin as BaseConstanceAdmin from constance.admin import ConstanceAdmin as BaseConstanceAdmin
from django.apps import apps from django.apps import apps

View file

@ -260,6 +260,7 @@ class BulkOrderAction(BaseMutation):
elif order_hr_id: elif order_hr_id:
order = Order.objects.get(user=user, human_readable_id=order_hr_id) order = Order.objects.get(user=user, human_readable_id=order_hr_id)
# noinspection PyUnreachableCode
match action: match action:
case "add": case "add":
order = order.bulk_add_products(products) order = order.bulk_add_products(products)

View file

@ -15,7 +15,7 @@ class Command(BaseCommand):
for product in Product.objects.filter(stocks__isnull=False): for product in Product.objects.filter(stocks__isnull=False):
for stock in product.stocks.all(): for stock in product.stocks.all():
try: try:
stock.price = AbstractVendor.round_price_marketologically(stock.price) stock.price = AbstractVendor.round_price_marketologically(stock.price) # type: ignore
stock.save() stock.save()
except Exception as e: except Exception as e:
self.stdout.write(self.style.WARNING(f"Couldn't fix price on {stock.uuid}")) self.stdout.write(self.style.WARNING(f"Couldn't fix price on {stock.uuid}"))

View file

@ -40,13 +40,13 @@ class Command(BaseCommand):
"-t", "-t",
"--target", "--target",
required=True, required=True,
help=("Dotted path to the field to translate, e.g. core.models.Product.description"), help="Dotted path to the field to translate, e.g. core.models.Product.description",
) )
parser.add_argument( parser.add_argument(
"-l", "-l",
"--language", "--language",
required=True, required=True,
help=("Modeltranslation language code to translate into, e.g. de-de, fr-fr, zh-hans"), help="Modeltranslation language code to translate into, e.g. de-de, fr-fr, zh-hans",
) )
def handle(self, *args, **options): def handle(self, *args, **options):

File diff suppressed because it is too large Load diff

View file

@ -27,8 +27,8 @@ from core.serializers.utility import AddressSerializer
class AttributeGroupSimpleSerializer(ModelSerializer): class AttributeGroupSimpleSerializer(ModelSerializer):
parent: PrimaryKeyRelatedField = PrimaryKeyRelatedField(read_only=True) parent: PrimaryKeyRelatedField = PrimaryKeyRelatedField(read_only=True) # type: ignore
children: PrimaryKeyRelatedField = PrimaryKeyRelatedField(many=True, read_only=True) children: PrimaryKeyRelatedField = PrimaryKeyRelatedField(many=True, read_only=True) # type: ignore
class Meta: class Meta:
model = AttributeGroup model = AttributeGroup

View file

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<!--suppress JSSuspiciousNameCombination --> <!--suppress JSSuspiciousNameCombination, JSUnresolvedReference -->
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>

View file

@ -160,7 +160,6 @@ def process_promotions() -> tuple[bool, str]:
:return: A tuple where the first element is a boolean indicating success, and the second :return: A tuple where the first element is a boolean indicating success, and the second
element is a message describing the operation's outcome. element is a message describing the operation's outcome.
:rtype: tuple[bool, str]
""" """
if not config.ABSTRACT_API_KEY or config.ABSTRACT_API_KEY == "example key": if not config.ABSTRACT_API_KEY or config.ABSTRACT_API_KEY == "example key":
return False, "Abstract features disabled." return False, "Abstract features disabled."

View file

@ -47,7 +47,7 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]:
except Order.DoesNotExist: except Order.DoesNotExist:
return False, f"Order not found with the given pk: {order_pk}" return False, f"Order not found with the given pk: {order_pk}"
activate(order.user.language) activate(order.user.language) # type: ignore
set_email_settings() set_email_settings()
connection = mail.get_connection() connection = mail.get_connection()
@ -64,7 +64,7 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]:
"total_price": order.total_price, "total_price": order.total_price,
}, },
), ),
to=[order.user.email], to=[order.user.email], # type: ignore
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>", from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
connection=connection, connection=connection,
) )
@ -80,7 +80,7 @@ def send_order_finished_email(order_pk: str) -> tuple[bool, str]:
if len(ops) <= 0: if len(ops) <= 0:
return return
activate(order.user.language) activate(order.user.language) # type: ignore
set_email_settings() set_email_settings()
connection = mail.get_connection() connection = mail.get_connection()
@ -91,16 +91,16 @@ def send_order_finished_email(order_pk: str) -> tuple[bool, str]:
template_name="digital_order_delivered_email.html", template_name="digital_order_delivered_email.html",
context={ context={
"order_uuid": order.human_readable_id, "order_uuid": order.human_readable_id,
"user_first_name": order.user.first_name, "user_first_name": order.user.first_name, # type: ignore
"order_products": ops, "order_products": ops,
"project_name": config.PROJECT_NAME, "project_name": config.PROJECT_NAME,
"contact_email": config.EMAIL_FROM, "contact_email": config.EMAIL_FROM,
"total_price": round(sum(op.buy_price for op in ops), 2), "total_price": round(sum(op.buy_price for op in ops), 2),
"display_system_attributes": order.user.has_perm("core.view_order"), "display_system_attributes": order.user.has_perm("core.view_order"), # type: ignore
"today": datetime.today(), "today": datetime.today(),
}, },
), ),
to=[order.user.email], to=[order.user.email], # type: ignore
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>", from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
connection=connection, connection=connection,
) )
@ -108,7 +108,7 @@ def send_order_finished_email(order_pk: str) -> tuple[bool, str]:
email.send() email.send()
def send_thank_you_email(ops: list[OrderProduct]): def send_thank_you_email(ops: list[OrderProduct]):
activate(order.user.language) activate(order.user.language) # type: ignore
set_email_settings() set_email_settings()

View file

@ -197,7 +197,7 @@ class AbstractVendor:
price = float(original_price) price = float(original_price)
if category and category.markup_percent: if category and category.markup_percent:
price *= 1 + category.markup_percent / 100.0 price *= 1 + float(category.markup_percent) / 100.0 # type: ignore
elif vendor and vendor.markup_percent: elif vendor and vendor.markup_percent:
price *= 1 + vendor.markup_percent / 100.0 price *= 1 + vendor.markup_percent / 100.0

View file

@ -42,6 +42,7 @@ class CustomLocaleMiddleware(LocaleMiddleware):
request.LANGUAGE_CODE = normalized request.LANGUAGE_CODE = normalized
# noinspection PyShadowingBuiltins
class GrapheneJWTAuthorizationMiddleware: class GrapheneJWTAuthorizationMiddleware:
def resolve(self, next, root, info, **args): def resolve(self, next, root, info, **args):
context = info.context context = info.context
@ -92,6 +93,7 @@ class BlockInvalidHostMiddleware:
return self.get_response(request) return self.get_response(request)
# noinspection PyShadowingBuiltins
class GrapheneLoggingErrorsDebugMiddleware: class GrapheneLoggingErrorsDebugMiddleware:
def resolve(self, next, root, info, **args): def resolve(self, next, root, info, **args):
try: try:

View file

@ -10,9 +10,9 @@ class CustomPagination(PageNumberPagination):
{ {
"links": {"forward": self.get_next_link(), "backward": self.get_previous_link()}, "links": {"forward": self.get_next_link(), "backward": self.get_previous_link()},
"counts": { "counts": {
"total_pages": None or self.page.paginator.num_pages, "total_pages": None or self.page.paginator.num_pages, # type: ignore
"page_size": None or self.page_size, "page_size": None or self.page_size, # type: ignore
"total_items": None or self.page.paginator.count, "total_items": None or self.page.paginator.count, # type: ignore
}, },
"data": data, "data": data,
} }

View file

@ -227,7 +227,7 @@ CURRENCIES: tuple[tuple[str, str], ...] = (
("zh-hans", "CNY"), ("zh-hans", "CNY"),
) )
CURRENCY_CODE: str = dict(CURRENCIES).get(LANGUAGE_CODE) CURRENCY_CODE: str | None = dict(CURRENCIES).get(LANGUAGE_CODE)
MODELTRANSLATION_FALLBACK_LANGUAGES: tuple = (LANGUAGE_CODE, "en-us", "de-de") MODELTRANSLATION_FALLBACK_LANGUAGES: tuple = (LANGUAGE_CODE, "en-us", "de-de")
@ -274,7 +274,7 @@ INTERNAL_IPS: list[str] = [
"127.0.0.1", "127.0.0.1",
] ]
DAISY_SETTINGS: dict[str, str | list[str] | dict[str, str] | None | bool | int | float | list[dict[str, str]]] = { DAISY_SETTINGS: dict = {
"SITE_LOGO": "/static/favicon.ico", "SITE_LOGO": "/static/favicon.ico",
"EXTRA_STYLES": [], "EXTRA_STYLES": [],
"EXTRA_SCRIPTS": [], "EXTRA_SCRIPTS": [],
@ -283,6 +283,10 @@ DAISY_SETTINGS: dict[str, str | list[str] | dict[str, str] | None | bool | int |
"DONT_SUPPORT_ME": True, "DONT_SUPPORT_ME": True,
"SIDEBAR_FOOTNOTE": "eVibes by Wiseless", "SIDEBAR_FOOTNOTE": "eVibes by Wiseless",
"APPS_REORDER": { "APPS_REORDER": {
"django_celery_results": {
"hide": True,
"app": "django_celery_results",
},
"django_celery_beat": { "django_celery_beat": {
"icon": "fa fa-solid fa-timeline", "icon": "fa fa-solid fa-timeline",
"hide": False, "hide": False,

View file

@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
from evibes.settings.base import * # noqa: F403 from evibes.settings.base import * # noqa: F403
from evibes.settings.constance import CONSTANCE_CONFIG from evibes.settings.constance import CONSTANCE_CONFIG
REST_FRAMEWORK: dict[str, int | str | dict[str, str | bool]] = { REST_FRAMEWORK: dict = {
"DEFAULT_PAGINATION_CLASS": "evibes.pagination.CustomPagination", "DEFAULT_PAGINATION_CLASS": "evibes.pagination.CustomPagination",
"PAGE_SIZE": 30, "PAGE_SIZE": 30,
"DEFAULT_AUTHENTICATION_CLASSES": [ "DEFAULT_AUTHENTICATION_CLASSES": [
@ -45,6 +45,7 @@ SIMPLE_JWT: dict[str, timedelta | str | bool] = {
} }
# type: ignore # type: ignore
# noinspection Mypy
SPECTACULAR_B2B_DESCRIPTION = _(f""" SPECTACULAR_B2B_DESCRIPTION = _(f"""
Welcome to the {CONSTANCE_CONFIG.get("PROJECT_NAME")[0]} B2B API documentation. Welcome to the {CONSTANCE_CONFIG.get("PROJECT_NAME")[0]} B2B API documentation.
@ -69,6 +70,7 @@ Current API version: {EVIBES_VERSION}
""") # noqa: E501, F405 """) # noqa: E501, F405
# type: ignore # type: ignore
# noinspection Mypy
SPECTACULAR_PLATFORM_DESCRIPTION = _(f""" SPECTACULAR_PLATFORM_DESCRIPTION = _(f"""
Welcome to the {CONSTANCE_CONFIG.get("PROJECT_NAME")[0]} Platform API documentation. Welcome to the {CONSTANCE_CONFIG.get("PROJECT_NAME")[0]} Platform API documentation.
@ -95,6 +97,7 @@ The {CONSTANCE_CONFIG.get("PROJECT_NAME")[0]} API is the central hub for managin
Current API version: {EVIBES_VERSION} Current API version: {EVIBES_VERSION}
""") # noqa: E501, F405 """) # noqa: E501, F405
# noinspection Mypy
SPECTACULAR_PLATFORM_SETTINGS = { SPECTACULAR_PLATFORM_SETTINGS = {
"TITLE": f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]} API", "TITLE": f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]} API",
"DESCRIPTION": SPECTACULAR_PLATFORM_DESCRIPTION, "DESCRIPTION": SPECTACULAR_PLATFORM_DESCRIPTION,
@ -146,6 +149,7 @@ SPECTACULAR_PLATFORM_SETTINGS = {
}, },
} }
# noinspection Mypy
SPECTACULAR_B2B_SETTINGS = { SPECTACULAR_B2B_SETTINGS = {
"TITLE": f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]} API", "TITLE": f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]} API",
"DESCRIPTION": SPECTACULAR_B2B_DESCRIPTION, "DESCRIPTION": SPECTACULAR_B2B_DESCRIPTION,

View file

@ -1,9 +1,9 @@
from evibes.settings import CONSTANCE_CONFIG from evibes.settings import CONSTANCE_CONFIG
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = CONSTANCE_CONFIG.get("EMAIL_HOST")[0] EMAIL_HOST = CONSTANCE_CONFIG.get("EMAIL_HOST")[0] # type: ignore
EMAIL_PORT = CONSTANCE_CONFIG.get("EMAIL_PORT")[0] EMAIL_PORT = CONSTANCE_CONFIG.get("EMAIL_PORT")[0] # type: ignore
EMAIL_USE_TLS = CONSTANCE_CONFIG.get("EMAIL_USE_TLS")[0] EMAIL_USE_TLS = CONSTANCE_CONFIG.get("EMAIL_USE_TLS")[0] # type: ignore
EMAIL_USE_SSL = CONSTANCE_CONFIG.get("EMAIL_USE_SSL")[0] EMAIL_USE_SSL = CONSTANCE_CONFIG.get("EMAIL_USE_SSL")[0] # type: ignore
EMAIL_HOST_USER = CONSTANCE_CONFIG.get("EMAIL_HOST_USER")[0] EMAIL_HOST_USER = CONSTANCE_CONFIG.get("EMAIL_HOST_USER")[0] # type: ignore
EMAIL_HOST_PASSWORD = CONSTANCE_CONFIG.get("EMAIL_HOST_PASSWORD")[0] EMAIL_HOST_PASSWORD = CONSTANCE_CONFIG.get("EMAIL_HOST_PASSWORD")[0] # type: ignore

View file

@ -6,7 +6,7 @@ LOGGING = {
"formatters": { "formatters": {
"color": { "color": {
"()": "colorlog.ColoredFormatter", "()": "colorlog.ColoredFormatter",
"format": ("%(asctime)s %(log_color)s[%(levelname)s]%(reset)s %(name)s: %(message)s"), "format": "%(asctime)s %(log_color)s[%(levelname)s]%(reset)s %(name)s: %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S", "datefmt": "%Y-%m-%d %H:%M:%S",
"log_colors": { "log_colors": {
"DEBUG": "cyan", "DEBUG": "cyan",

View file

@ -1,3 +1,4 @@
# noinspection PyUnresolvedReferences
from django.contrib import admin from django.contrib import admin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _

View file

@ -4,11 +4,13 @@ from django.db.models import CASCADE, CharField, FloatField, ForeignKey, JSONFie
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.abstract import NiceModel from core.abstract import NiceModel
from core.models import Order
from vibes_auth.models import User
class Balance(NiceModel): class Balance(NiceModel):
amount: FloatField = FloatField(null=False, blank=False, default=0) amount: float = FloatField(null=False, blank=False, default=0)
user: OneToOneField = OneToOneField( user: User = OneToOneField(
to="vibes_auth.User", on_delete=CASCADE, blank=True, null=True, related_name="payments_balance" to="vibes_auth.User", on_delete=CASCADE, blank=True, null=True, related_name="payments_balance"
) )
@ -26,11 +28,11 @@ class Balance(NiceModel):
class Transaction(NiceModel): class Transaction(NiceModel):
amount: FloatField = FloatField(null=False, blank=False) amount: float = FloatField(null=False, blank=False)
balance: ForeignKey = ForeignKey(Balance, on_delete=CASCADE, blank=True, null=True, related_name="transactions") balance: Balance = ForeignKey(Balance, on_delete=CASCADE, blank=True, null=True, related_name="transactions")
currency: CharField = CharField(max_length=3, null=False, blank=False) currency: str = CharField(max_length=3, null=False, blank=False)
payment_method: CharField = CharField(max_length=20, null=True, blank=True) payment_method: str = CharField(max_length=20, null=True, blank=True)
order: ForeignKey = ForeignKey( order: Order = ForeignKey(
"core.Order", "core.Order",
on_delete=CASCADE, on_delete=CASCADE,
blank=True, blank=True,
@ -38,7 +40,7 @@ class Transaction(NiceModel):
help_text=_("order to process after paid"), help_text=_("order to process after paid"),
related_name="payments_transactions", related_name="payments_transactions",
) )
process: JSONField = JSONField(verbose_name=_("processing details"), default=dict) process: dict = JSONField(verbose_name=_("processing details"), default=dict)
def __str__(self): def __str__(self):
return f"{self.balance.user.email} | {self.amount}" return f"{self.balance.user.email} | {self.amount}"

View file

@ -20,10 +20,10 @@ class TransactionProcessSerializer(ModelSerializer):
order_uuid = SerializerMethodField(read_only=True, required=False) order_uuid = SerializerMethodField(read_only=True, required=False)
def get_order_hr_id(self, obj: Transaction): def get_order_hr_id(self, obj: Transaction):
return obj.order.human_readable_id if obj.order else None return obj.order.human_readable_id if obj.order else None # type: ignore
def get_order_uuid(self, obj: Transaction): def get_order_uuid(self, obj: Transaction):
return obj.order.uuid if obj.order else None return obj.order.uuid if obj.order else None # type: ignore
class Meta: class Meta:
model = Transaction model = Transaction

View file

@ -18,6 +18,7 @@ from payments.views import CallbackAPIView, DepositView
############################################################################### ###############################################################################
# noinspection PyArgumentList
class BalanceModelTests(TestCase): class BalanceModelTests(TestCase):
def setUp(self): def setUp(self):
self.user_model = get_user_model() self.user_model = get_user_model()
@ -34,9 +35,10 @@ class BalanceModelTests(TestCase):
self.balance.save() self.balance.save()
self.balance.refresh_from_db() self.balance.refresh_from_db()
# round(10.129, 2) == 10.13 # round(10.129, 2) == 10.13
self.assertAlmostEqual(self.balance.amount, 10.13, places=2) self.assertAlmostEqual(float(self.balance.amount), 10.13, places=2)
# noinspection PyArgumentList
class TransactionModelTests(TestCase): class TransactionModelTests(TestCase):
def setUp(self): def setUp(self):
self.user_model = get_user_model() self.user_model = get_user_model()
@ -66,6 +68,7 @@ class TransactionModelTests(TestCase):
############################################################################### ###############################################################################
# noinspection PyArgumentList
class DepositViewTests(TestCase): class DepositViewTests(TestCase):
def setUp(self): def setUp(self):
self.factory = APIRequestFactory() self.factory = APIRequestFactory()
@ -115,6 +118,7 @@ class CallbackViewTests(TestCase):
############################################################################### ###############################################################################
# noinspection PyArgumentList
class SignalTests(TestCase): class SignalTests(TestCase):
def setUp(self): def setUp(self):
self.user_model = get_user_model() self.user_model = get_user_model()
@ -133,6 +137,7 @@ class SignalTests(TestCase):
############################################################################### ###############################################################################
# noinspection PyArgumentList
class GraphQLDepositTests(TestCase): class GraphQLDepositTests(TestCase):
def setUp(self): def setUp(self):
self.user_model = get_user_model() self.user_model = get_user_model()

View file

@ -7,6 +7,7 @@ def get_rates(provider: str):
if not provider: if not provider:
raise ValueError(_("a provider to get rates from is required")) raise ValueError(_("a provider to get rates from is required"))
# noinspection PyUnreachableCode
match provider: match provider:
case "cbr": case "cbr":
return get_rates_cbr() return get_rates_cbr()

View file

@ -20,7 +20,8 @@ def balance_email(user_pk: str) -> tuple[bool, str]:
email = EmailMessage( email = EmailMessage(
"eVibes | Successful Order", "eVibes | Successful Order",
render_to_string("balance.html", {"user": user, "current_year": timezone.now().year, "config": config}), render_to_string("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

@ -37,6 +37,7 @@ class CallbackAPIView(APIView):
logger.debug(request.__dict__) logger.debug(request.__dict__)
try: try:
gateway = kwargs.get("gateway", "") gateway = kwargs.get("gateway", "")
# noinspection PyUnreachableCode
match gateway: match gateway:
case "gateway": case "gateway":
# Gateway.process_callback(request.data) # Gateway.process_callback(request.data)

View file

@ -7,110 +7,111 @@ readme = "README.md"
package-mode = false package-mode = false
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.poetry.dependencies] [tool.poetry.dependencies]
aiosmtpd = "1.4.6" aiosmtpd = "1.4.6"
celery = { version = "5.5.2", optional = true } celery = { version = "5.5.2", optional = true }
celery-prometheus-exporter = { version = "1.7.0", optional = true } celery-prometheus-exporter = { version = "1.7.0", optional = true }
colorlog = "6.9.0" colorlog = "6.9.0"
cryptography = "44.0.3" cryptography = "44.0.3"
django = "5.2" django = "5.2"
django-cacheops = "7.2" django-cacheops = "7.2"
django-celery-beat = { version = "2.8.0", optional = true } django-celery-beat = { version = "2.8.0", optional = true }
django-celery-results = { version = "2.6.0", optional = true } django-celery-results = { version = "2.6.0", optional = true }
django-constance = "4.3.2" django-constance = "4.3.2"
django-cors-headers = "4.7.0" django-cors-headers = "4.7.0"
django-daisy = "1.0.23" django-daisy = "1.0.23"
django-dbbackup = "4.2.1" django-dbbackup = "4.2.1"
django-elasticsearch-dsl = "8.0" django-elasticsearch-dsl = "8.0"
django-elasticsearch-dsl-drf = "0.22.5" django-elasticsearch-dsl-drf = "0.22.5"
django-extensions = "4.1" django-extensions = "4.1"
django-filter = "25.1" django-filter = "25.1"
django-health-check = "3.19.1" django-health-check = "3.19.1"
django-hosts = "6.0" django-hosts = "6.0"
django-json-widget = "2.0.1" django-json-widget = "2.0.1"
django-mailbox = "4.10.1" django-mailbox = "4.10.1"
django-model-utils = "5.0.0" django-model-utils = "5.0.0"
django-modeltranslation = "0.19.14" django-modeltranslation = "0.19.14"
django-mptt = "0.17.0" django-mptt = "0.17.0"
django-prometheus = "^2.3.1" django-prometheus = "^2.3.1"
django-redis = "5.4.0" django-redis = "5.4.0"
django-ratelimit = "4.1.0" django-ratelimit = "4.1.0"
django-storages = "1.14.6" django-storages = "1.14.6"
django-stubs = "5.2.0" django-stubs = "5.2.0"
django-widget-tweaks = "1.5.0" django-widget-tweaks = "1.5.0"
django-md-field = "0.1.0" django-md-field = "0.1.0"
djangorestframework = "3.16.0" djangorestframework = "3.16.0"
djangorestframework-camel-case = "1.4.2" djangorestframework-camel-case = "1.4.2"
djangorestframework-recursive = "0.1.2" djangorestframework-recursive = "0.1.2"
djangorestframework-simplejwt = { extras = ["crypto"], version = "5.5.0" } djangorestframework-simplejwt = { extras = ["crypto"], version = "5.5.0" }
djangorestframework-stubs = "3.16.0" djangorestframework-stubs = "3.16.0"
djangorestframework-xml = "2.0.0" djangorestframework-xml = "2.0.0"
djangorestframework-yaml = "2.0.0" djangorestframework-yaml = "2.0.0"
drf-spectacular = { extras = ["sidecar"], version = "0.28.0" } drf-spectacular = { extras = ["sidecar"], version = "0.28.0" }
elasticsearch-dsl = "8.18.0" elasticsearch-dsl = "8.18.0"
filetype = "1.2.0" filetype = "1.2.0"
graphene-django = "3.2.3" graphene-django = "3.2.3"
graphene-file-upload = "1.3.0" graphene-file-upload = "1.3.0"
gunicorn = "23.0.0" gunicorn = "23.0.0"
httpx = "0.28.1" httpx = "0.28.1"
jupyter = { version = "1.1.1", optional = true } jupyter = { version = "1.1.1", optional = true }
mypy = "1.16.1" mypy = "1.16.1"
openai = { version = "1.77.0", optional = true } openai = { version = "1.77.0", optional = true }
paramiko = "3.5.1" paramiko = "3.5.1"
pillow = "11.2.1" pillow = "11.2.1"
polib = "1.2.0" polib = "1.2.0"
poetry-core = "2.1.3" poetry-core = "2.1.3"
python = ">=3.12,<3.13" python = ">=3.12,<3.13"
psutil = "6.1.1" psutil = "6.1.1"
psycopg2 = "2.9.10" psycopg2 = "2.9.10"
pygraphviz = { version = "1.14", optional = true, markers = "sys_platform != 'win32'"} pygraphviz = { version = "1.14", optional = true, markers = "sys_platform != 'win32'" }
pymdown-extensions = "10.15" pymdown-extensions = "10.15"
redis = "6.0.0" redis = "6.0.0"
requests = "2.32.3" requests = "2.32.3"
ruff = "0.11.8" ruff = "0.11.8"
sentry-sdk = { extras = ["django", "celery", "opentelemetry", "redis"], version = "2.27.0" } sentry-sdk = { extras = ["django", "celery", "opentelemetry", "redis"], version = "2.27.0" }
six = "1.17.0" six = "1.17.0"
swapper = "1.4.0" swapper = "1.4.0"
zeep = "4.3.1" zeep = "4.3.1"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
black = "25.1.0" black = "25.1.0"
celery-stubs = "0.1.3" celery-stubs = "0.1.3"
isort = "5.13.2" isort = "5.13.2"
flake8 = "7.2.0" flake8 = "7.2.0"
mypy-extensions = "1.1.0" mypy-extensions = "1.1.0"
pytest = "8.4.1" pytest = "8.4.1"
pytest-django = "4.11.1" pytest-django = "4.11.1"
coverage = "7.8.2" coverage = "7.8.2"
pre-commit = "4.2.0" pre-commit = "4.2.0"
safety = "3.5.2" safety = "3.5.2"
bandit = "1.2.0" bandit = "1.2.0"
[tool.poetry.extras] [tool.poetry.extras]
graph = ["pygraphviz"] graph = ["pygraphviz"]
worker = ["celery", "django-celery-beat", "django-celery-results", "celery-prometheus-exporter"] worker = ["celery", "django-celery-beat", "django-celery-results", "celery-prometheus-exporter"]
openai = ["openai"] openai = ["openai"]
jupyter = ["jupyter"] jupyter = ["jupyter"]
docs = ["sphinx", "sphinx-rtd-theme", "m2r2"] docs = ["sphinx", "sphinx-rtd-theme", "m2r2"]
testing = ["pytest", "pytest-django", "coverage"] testing = ["pytest", "pytest-django", "coverage"]
linting = ["black", "isort", "flake8", "bandit"] linting = ["black", "isort", "flake8", "bandit"]
[tool.mypy] [tool.mypy]
disable_error_code = ["import-untyped", "misc"] disable_error_code = ["import-untyped", "misc"]
exclude = ["*/migrations/*"]
[tool.ruff] [tool.ruff]
line-length = 120 line-length = 120
target-version = "py38" target-version = "py38"
exclude = ["migrations", "media", "static", "storefront"] exclude = ["migrations", "media", "static", "storefront"]
[tool.ruff.lint] [tool.ruff.lint]
select = ["E", "W", "F", "B", "I", "RUF", "UP", "N", "A", "COM", "C4", "DJ001", "RSE", "SIM", "ISC", "TID252", "PGH004"] select = ["E", "W", "F", "B", "I", "RUF", "UP", "N", "A", "COM", "C4", "DJ001", "RSE", "SIM", "ISC", "TID252", "PGH004"]
ignore = ["B904", "RUF001", "RUF002", "RUF003", "RUF005", "RUF012", "A003", "A002", "COM812", "S603"] ignore = ["B904", "RUF001", "RUF002", "RUF003", "RUF005", "RUF012", "A003", "A002", "COM812", "S603"]
per-file-ignores = { "__init__.py" = ["E402", "F401"] } per-file-ignores = { "__init__.py" = ["E402", "F401"] }
[tool.ruff.format] [tool.ruff.format]
quote-style = "double" quote-style = "double"
indent-style = "space" indent-style = "space"

View file

@ -41,6 +41,7 @@ class UserManager(BaseUserManager):
def _create_user(self, email, password, **extra_fields): def _create_user(self, email, password, **extra_fields):
email = self.normalize_email(email) email = self.normalize_email(email)
# noinspection PyShadowingNames
user = self.model(email=email, **extra_fields) user = self.model(email=email, **extra_fields)
user.password = make_password(password) user.password = make_password(password)
user.save(using=self._db) user.save(using=self._db)
@ -55,10 +56,11 @@ class UserManager(BaseUserManager):
def create_superuser(self, email=None, password=None, **extra_fields): def create_superuser(self, email=None, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True) extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_staff") is not True: if not extra_fields.get("is_staff"):
raise ValueError("Superuser must have is_staff=True.") raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True: if not extra_fields.get("is_superuser"):
raise ValueError("Superuser must have is_superuser=True.") raise ValueError("Superuser must have is_superuser=True.")
# noinspection PyShadowingNames
user = self._create_user(email, password, **extra_fields) user = self._create_user(email, password, **extra_fields)
user.is_active = True user.is_active = True
user.is_verified = True user.is_verified = True

View file

@ -1,3 +1,4 @@
import uuid
from uuid import uuid4 from uuid import uuid4
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
@ -29,8 +30,8 @@ class User(AbstractUser, NiceModel):
def get_uuid_as_path(self, *args): def get_uuid_as_path(self, *args):
return str(self.uuid) + "/" + args[0] return str(self.uuid) + "/" + args[0]
email: EmailField = EmailField(_("email"), unique=True, help_text=_("user email address")) email: str = EmailField(_("email"), unique=True, help_text=_("user email address"))
phone_number: CharField = CharField( phone_number: str = CharField(
_("phone_number"), _("phone_number"),
max_length=20, max_length=20,
unique=True, unique=True,
@ -42,9 +43,9 @@ class User(AbstractUser, NiceModel):
], ],
) )
username = None username = None
first_name: CharField = CharField(_("first_name"), max_length=150, blank=True, null=True) first_name: str = CharField(_("first_name"), max_length=150, blank=True, null=True)
last_name: CharField = CharField(_("last_name"), max_length=150, blank=True, null=True) last_name: str = CharField(_("last_name"), max_length=150, blank=True, null=True)
avatar: ImageField = ImageField( avatar = ImageField(
null=True, null=True,
verbose_name=_("avatar"), verbose_name=_("avatar"),
upload_to=get_uuid_as_path, upload_to=get_uuid_as_path,
@ -52,27 +53,28 @@ class User(AbstractUser, NiceModel):
help_text=_("user profile image"), help_text=_("user profile image"),
) )
is_verified: BooleanField = BooleanField( is_verified: bool = BooleanField(
default=False, default=False,
verbose_name=_("is verified"), verbose_name=_("is verified"),
help_text=_("user verification status"), help_text=_("user verification status"),
) )
is_active: BooleanField = BooleanField( is_active: bool = BooleanField(
_("is_active"), _("is_active"),
default=False, default=False,
help_text=_("unselect this instead of deleting accounts"), help_text=_("unselect this instead of deleting accounts"),
) )
is_subscribed: BooleanField = BooleanField( is_subscribed: bool = BooleanField(
verbose_name=_("is_subscribed"), help_text=_("user's newsletter subscription status"), default=False verbose_name=_("is_subscribed"), help_text=_("user's newsletter subscription status"), default=False
) )
activation_token: UUIDField = UUIDField(default=uuid4, verbose_name=_("activation token")) activation_token: uuid = UUIDField(default=uuid4, verbose_name=_("activation token"))
language: CharField = CharField(choices=LANGUAGES, default=LANGUAGE_CODE, null=False, blank=False, max_length=7) language: str = CharField(choices=LANGUAGES, default=LANGUAGE_CODE, null=False, blank=False, max_length=7)
attributes: JSONField = JSONField(verbose_name=_("attributes"), default=dict, blank=True, null=True) attributes: dict = JSONField(verbose_name=_("attributes"), default=dict, blank=True, null=True)
USERNAME_FIELD = "email" USERNAME_FIELD = "email"
REQUIRED_FIELDS = [] REQUIRED_FIELDS = []
objects = UserManager() # type: ignore # noinspection PyClassVar
objects = UserManager()
def add_to_recently_viewed(self, product_uuid): def add_to_recently_viewed(self, product_uuid):
recently_viewed = self.recently_viewed recently_viewed = self.recently_viewed

View file

@ -122,12 +122,12 @@ class TokenObtainSerializer(Serializer):
with suppress(KeyError): with suppress(KeyError):
authenticate_kwargs["request"] = self.context["request"] authenticate_kwargs["request"] = self.context["request"]
self.user = authenticate(**authenticate_kwargs) self.user = authenticate(**authenticate_kwargs) # type: ignore
if not api_settings.USER_AUTHENTICATION_RULE(self.user): if not api_settings.USER_AUTHENTICATION_RULE(self.user):
raise AuthenticationFailed( raise AuthenticationFailed(
self.error_messages["no_active_account"], self.error_messages["no_active_account"],
_("no active account"), _("no active account"), # type: ignore
) )
return {} return {}
@ -145,15 +145,15 @@ class TokenObtainPairSerializer(TokenObtainSerializer):
logger.debug("Data validated") logger.debug("Data validated")
refresh = self.get_token(self.user) refresh = self.get_token(self.user) # type: ignore
data["refresh"] = str(refresh) data["refresh"] = str(refresh)
data["access"] = str(refresh.access_token) data["access"] = str(refresh.access_token) # type: ignore
data["user"] = UserSerializer(self.user).data data["user"] = UserSerializer(self.user).data
logger.debug("Data formed") logger.debug("Data formed")
if api_settings.UPDATE_LAST_LOGIN: if api_settings.UPDATE_LAST_LOGIN:
update_last_login(self.user, self.user) update_last_login(self.user, self.user) # type: ignore
logger.debug("Updated last login") logger.debug("Updated last login")
logger.debug("Returning data") logger.debug("Returning data")
@ -181,7 +181,7 @@ class TokenRefreshSerializer(Serializer):
data["refresh"] = str(refresh) data["refresh"] = str(refresh)
user = User.objects.get(uuid=refresh.payload["user_uuid"]) user = User.objects.get(uuid=refresh.payload["user_uuid"])
data["user"] = UserSerializer(user).data data["user"] = UserSerializer(user).data # type: ignore
return data return data
@ -213,7 +213,7 @@ class TokenVerifySerializer(Serializer):
except User.DoesNotExist: except User.DoesNotExist:
raise ValidationError(_("user does not exist")) raise ValidationError(_("user does not exist"))
attrs["user"] = UserSerializer(user).data attrs["user"] = UserSerializer(user).data # type: ignore
return attrs return attrs