From e0e3c93e63fe7d5b683014ddc50d0c3e4825b133 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Thu, 5 Mar 2026 00:07:17 +0300 Subject: [PATCH 1/3] refactor(scripts): centralize static files collection in Docker entrypoint Moved static files collection from platform-specific scripts to the Docker entrypoint script. Simplifies maintenance by eliminating redundant logic across Windows, Unix, and restart scripts. Updated logo/icon settings to use `static()` for better flexibility. --- schon/settings/unfold.py | 4 ++-- scripts/Docker/app-entrypoint.sh | 1 + scripts/Unix/restart.sh | 7 ------- scripts/Unix/run.sh | 7 ------- scripts/Windows/restart.ps1 | 8 -------- scripts/Windows/run.ps1 | 8 -------- 6 files changed, 3 insertions(+), 32 deletions(-) diff --git a/schon/settings/unfold.py b/schon/settings/unfold.py index 1f9eac98..69d4d1a0 100644 --- a/schon/settings/unfold.py +++ b/schon/settings/unfold.py @@ -34,8 +34,8 @@ UNFOLD: dict[str, Any] = { "SITE_URL": STOREFRONT_DOMAIN, "SITE_TITLE": f"{PROJECT_NAME} Dashboard", "SITE_HEADER": PROJECT_NAME, - "SITE_LOGO": "favicon.png", - "SITE_ICON": "favicon.ico", + "SITE_LOGO": lambda request: static("favicon.png"), + "SITE_ICON": lambda request: static("favicon.ico"), "SITE_SYMBOL": "money", "SITE_DROPDOWN": [ { diff --git a/scripts/Docker/app-entrypoint.sh b/scripts/Docker/app-entrypoint.sh index 07a8c203..bdd0e40e 100644 --- a/scripts/Docker/app-entrypoint.sh +++ b/scripts/Docker/app-entrypoint.sh @@ -2,6 +2,7 @@ set -e uv run manage.py await_services +uv run manage.py collectstatic --noinput --verbosity 0 if [ "${DEBUG:-0}" = "1" ]; then uv run uvicorn --host 0.0.0.0 --port 8000 --reload --log-level debug --ws-ping-interval 20 --ws-ping-timeout 20 schon.asgi:application diff --git a/scripts/Unix/restart.sh b/scripts/Unix/restart.sh index 5023ceeb..9b6db910 100755 --- a/scripts/Unix/restart.sh +++ b/scripts/Unix/restart.sh @@ -56,13 +56,6 @@ if ! output=$(docker compose exec -T app uv run manage.py search_index --rebuild exit 1 fi -log_info " → Collecting static files..." -if ! output=$(docker compose exec -T app uv run manage.py collectstatic --clear --no-input --verbosity 0 2>&1); then - log_error "Static files collection failed" - echo "$output" - exit 1 -fi - log_success "Pre-run tasks completed successfully!" # Cleanup diff --git a/scripts/Unix/run.sh b/scripts/Unix/run.sh index 0702e4ad..2c0ba267 100755 --- a/scripts/Unix/run.sh +++ b/scripts/Unix/run.sh @@ -64,13 +64,6 @@ if ! output=$(docker compose exec -T app uv run manage.py search_index --rebuild exit 1 fi -log_info " → Collecting static files..." -if ! output=$(docker compose exec -T app uv run manage.py collectstatic --clear --no-input --verbosity 0 2>&1); then - log_error "Static files collection failed" - echo "$output" - exit 1 -fi - log_success "Pre-run tasks completed successfully!" # Cleanup diff --git a/scripts/Windows/restart.ps1 b/scripts/Windows/restart.ps1 index fd248985..18f38b53 100644 --- a/scripts/Windows/restart.ps1 +++ b/scripts/Windows/restart.ps1 @@ -69,14 +69,6 @@ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -Write-Info " Collecting static files..." -$output = docker compose exec -T app uv run manage.py collectstatic --clear --no-input --verbosity 0 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-Error-Custom "Static files collection failed" - Write-Host $output - exit $LASTEXITCODE -} - Write-Success "Pre-run tasks completed successfully!" # Cleanup diff --git a/scripts/Windows/run.ps1 b/scripts/Windows/run.ps1 index 9c75cf3e..e7e57f0b 100644 --- a/scripts/Windows/run.ps1 +++ b/scripts/Windows/run.ps1 @@ -84,14 +84,6 @@ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -Write-Info " Collecting static files..." -$output = docker compose exec -T app uv run manage.py collectstatic --clear --no-input --verbosity 0 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-Error-Custom "Static files collection failed" - Write-Host $output - exit $LASTEXITCODE -} - Write-Success "Pre-run tasks completed successfully!" # Cleanup From 6955e6bec688b480bb95b3799b83da1fb268937c Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Thu, 5 Mar 2026 11:03:27 +0300 Subject: [PATCH 2/3] feat(models/serializers): migrate to `Decimal` for monetary values Replaced `float` with `Decimal` for monetary calculations to prevent precision loss and enhance accuracy. Updated related models, serializers, and utility functions accordingly. --- engine/core/models.py | 14 ++++++------ engine/core/serializers/detail.py | 37 ++++++++++++++----------------- engine/core/serializers/simple.py | 24 +++++++++----------- engine/core/utils/seo_builders.py | 2 +- engine/core/views.py | 16 +++++++++++++ 5 files changed, 52 insertions(+), 41 deletions(-) diff --git a/engine/core/models.py b/engine/core/models.py index e044f437..7cba3f71 100644 --- a/engine/core/models.py +++ b/engine/core/models.py @@ -469,21 +469,21 @@ class Category(NiceModel, MPTTModel): ).distinct() @cached_property - def min_price(self) -> float: + def min_price(self) -> Decimal: return ( self.products.filter(is_active=True, stocks__is_active=True).aggregate( Min("stocks__price") )["stocks__price__min"] - or 0.0 + or Decimal("0.00") ) @cached_property - def max_price(self) -> float: + def max_price(self) -> Decimal: return ( self.products.filter(is_active=True, stocks__is_active=True).aggregate( Max("stocks__price") )["stocks__price__max"] - or 0.0 + or Decimal("0.00") ) @cached_property @@ -776,7 +776,7 @@ class Product(NiceModel): ).order_by("-discount_percent") @cached_property - def discount_price(self) -> float | None: + def discount_price(self) -> Decimal | None: promo = self.promos.first() return (self.price / 100) * promo.discount_percent if promo else None @@ -795,11 +795,11 @@ class Product(NiceModel): return Feedback.objects.filter(order_product__product_id=self.pk).count() @property - def price(self: Self) -> float: + def price(self: Self) -> Decimal: stock = ( self.stocks.filter(is_active=True).only("price").order_by("-price").first() ) - return round(stock.price, 2) if stock else 0.0 + return Decimal(round(stock.price, 2)) if stock else Decimal("0.00") @cached_property def quantity(self) -> int: diff --git a/engine/core/serializers/detail.py b/engine/core/serializers/detail.py index 8d560611..1f229fe8 100644 --- a/engine/core/serializers/detail.py +++ b/engine/core/serializers/detail.py @@ -3,7 +3,7 @@ from contextlib import suppress from typing import Any from drf_spectacular.utils import extend_schema_field -from rest_framework.fields import JSONField, SerializerMethodField +from rest_framework.fields import DecimalField, JSONField, SerializerMethodField from rest_framework.serializers import ListSerializer, ModelSerializer from rest_framework_recursive.fields import RecursiveField @@ -64,8 +64,12 @@ class CategoryDetailSerializer(ModelSerializer): description = SerializerMethodField() filterable_attributes = SerializerMethodField() brands = BrandSimpleSerializer(many=True, read_only=True) - min_price = SerializerMethodField() - max_price = SerializerMethodField() + min_price = DecimalField( + max_digits=12, decimal_places=2, read_only=True, coerce_to_string=False + ) + max_price = DecimalField( + max_digits=12, decimal_places=2, read_only=True, coerce_to_string=False + ) class Meta: model = Category @@ -107,11 +111,6 @@ class CategoryDetailSerializer(ModelSerializer): return list(serializer.data) return [] - def get_min_price(self, obj: Category) -> float: - return obj.min_price - - def get_max_price(self, obj: Category) -> float: - return obj.max_price class BrandDetailSerializer(ModelSerializer): @@ -281,10 +280,15 @@ class ProductDetailSerializer(ModelSerializer): description = SerializerMethodField() rating = SerializerMethodField() - price = SerializerMethodField() + price = DecimalField( + max_digits=12, decimal_places=2, read_only=True, coerce_to_string=False + ) quantity = SerializerMethodField() feedbacks_count = SerializerMethodField() - discount_price = SerializerMethodField() + discount_price = DecimalField( + max_digits=12, decimal_places=2, read_only=True, allow_null=True, + coerce_to_string=False, + ) personal_orders_only = SerializerMethodField() class Meta: @@ -319,9 +323,6 @@ class ProductDetailSerializer(ModelSerializer): def get_rating(self, obj: Product) -> float: return obj.rating - def get_price(self, obj: Product) -> float: - return obj.price - def get_feedbacks_count(self, obj: Product) -> int: return obj.feedbacks_count @@ -331,9 +332,6 @@ class ProductDetailSerializer(ModelSerializer): def get_quantity(self, obj: Product) -> int: return obj.quantity - def get_discount_price(self, obj: Product) -> float | None: - return obj.discount_price - class PromotionDetailSerializer(ModelSerializer): products = ProductDetailSerializer( @@ -416,7 +414,9 @@ class OrderDetailSerializer(ModelSerializer): order_products = OrderProductDetailSerializer( many=True, ) - total_price = SerializerMethodField(read_only=True) + total_price = DecimalField( + max_digits=12, decimal_places=2, read_only=True, coerce_to_string=False + ) class Meta: model = Order @@ -433,6 +433,3 @@ class OrderDetailSerializer(ModelSerializer): "created", "modified", ] - - def get_total_price(self, obj: Order) -> float: - return obj.total_price # ty: ignore[invalid-return-type] diff --git a/engine/core/serializers/simple.py b/engine/core/serializers/simple.py index 249d2da5..945b4717 100644 --- a/engine/core/serializers/simple.py +++ b/engine/core/serializers/simple.py @@ -1,6 +1,6 @@ from typing import Any -from rest_framework.fields import JSONField, SerializerMethodField +from rest_framework.fields import DecimalField, JSONField, SerializerMethodField from rest_framework.relations import PrimaryKeyRelatedField from rest_framework.serializers import ModelSerializer @@ -140,11 +140,16 @@ class ProductSimpleSerializer(ModelSerializer): description = SerializerMethodField() rating = SerializerMethodField() - price = SerializerMethodField() + price = DecimalField( + max_digits=12, decimal_places=2, read_only=True, coerce_to_string=False + ) quantity = SerializerMethodField() feedbacks_count = SerializerMethodField() personal_orders_only = SerializerMethodField() - discount_price = SerializerMethodField() + discount_price = DecimalField( + max_digits=12, decimal_places=2, read_only=True, allow_null=True, + coerce_to_string=False, + ) class Meta: model = Product @@ -176,9 +181,6 @@ class ProductSimpleSerializer(ModelSerializer): def get_rating(self, obj: Product) -> float: return obj.rating - def get_price(self, obj: Product) -> float: - return obj.price - def get_feedbacks_count(self, obj: Product) -> int: return obj.feedbacks_count @@ -188,9 +190,6 @@ class ProductSimpleSerializer(ModelSerializer): def get_personal_orders_only(self, obj: Product) -> bool: return obj.personal_orders_only - def get_discount_price(self, obj: Product) -> float | None: - return obj.discount_price - class VendorSimpleSerializer(ModelSerializer): class Meta: @@ -286,7 +285,9 @@ class OrderSimpleSerializer(ModelSerializer): billing_address = AddressSerializer(read_only=True, required=False) shipping_address = AddressSerializer(read_only=True, required=False) attributes = JSONField(required=False) - total_price = SerializerMethodField(read_only=True) + total_price = DecimalField( + max_digits=12, decimal_places=2, read_only=True, coerce_to_string=False + ) class Meta: model = Order @@ -305,6 +306,3 @@ class OrderSimpleSerializer(ModelSerializer): "created", "modified", ] - - def get_total_price(self, obj: Order) -> float: - return obj.total_price # ty: ignore[invalid-return-type] diff --git a/engine/core/utils/seo_builders.py b/engine/core/utils/seo_builders.py index 58f59b9f..00cb960e 100644 --- a/engine/core/utils/seo_builders.py +++ b/engine/core/utils/seo_builders.py @@ -56,7 +56,7 @@ def product_schema(product, images, rating=None): offers.append( { "@type": "Offer", - "price": round(stock.price, 2), + "price": float(round(stock.price, 2)), "priceCurrency": settings.CURRENCY_CODE, "availability": "https://schema.org/InStock" if stock.quantity > 0 diff --git a/engine/core/views.py b/engine/core/views.py index 43a4b447..6eba249b 100644 --- a/engine/core/views.py +++ b/engine/core/views.py @@ -135,6 +135,22 @@ class CustomGraphQLView(FileUploadGraphQLView): def get_context(self, request): return request + @staticmethod + def json_encode(request, d, pretty=False): + from decimal import Decimal + + import orjson + + def _default(obj): + if isinstance(obj, Decimal): + return float(obj) + raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable") + + opts = orjson.OPT_NON_STR_KEYS + if pretty or request.GET.get("pretty"): + opts |= orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS + return orjson.dumps(d, default=_default, option=opts).decode("utf-8") + @extend_schema_view(**CUSTOM_OPENAPI_SCHEMA) class CustomSpectacularAPIView(SpectacularAPIView): From 31d9ccb82a9111756360deabfa4aeb76a8e6ff8a Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Thu, 5 Mar 2026 11:03:45 +0300 Subject: [PATCH 3/3] refactor(core): simplify code for price aggregation and formatting Improve readability by removing unnecessary parentheses in `min_price` and `max_price` methods in `models.py`. Enhance JSON serialization error formatting in `views.py` and adjustment of field formatting in serializers. No functional changes, purely stylistic updates for code maintainability. --- engine/core/models.py | 18 ++++++------------ engine/core/serializers/detail.py | 6 ++++-- engine/core/serializers/simple.py | 5 ++++- engine/core/views.py | 4 +++- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/engine/core/models.py b/engine/core/models.py index 7cba3f71..06f15426 100644 --- a/engine/core/models.py +++ b/engine/core/models.py @@ -470,21 +470,15 @@ class Category(NiceModel, MPTTModel): @cached_property def min_price(self) -> Decimal: - return ( - self.products.filter(is_active=True, stocks__is_active=True).aggregate( - Min("stocks__price") - )["stocks__price__min"] - or Decimal("0.00") - ) + return self.products.filter(is_active=True, stocks__is_active=True).aggregate( + Min("stocks__price") + )["stocks__price__min"] or Decimal("0.00") @cached_property def max_price(self) -> Decimal: - return ( - self.products.filter(is_active=True, stocks__is_active=True).aggregate( - Max("stocks__price") - )["stocks__price__max"] - or Decimal("0.00") - ) + return self.products.filter(is_active=True, stocks__is_active=True).aggregate( + Max("stocks__price") + )["stocks__price__max"] or Decimal("0.00") @cached_property def seo_description(self) -> str: diff --git a/engine/core/serializers/detail.py b/engine/core/serializers/detail.py index 1f229fe8..34f73440 100644 --- a/engine/core/serializers/detail.py +++ b/engine/core/serializers/detail.py @@ -112,7 +112,6 @@ class CategoryDetailSerializer(ModelSerializer): return [] - class BrandDetailSerializer(ModelSerializer): categories = CategorySimpleSerializer(many=True) description = SerializerMethodField() @@ -286,7 +285,10 @@ class ProductDetailSerializer(ModelSerializer): quantity = SerializerMethodField() feedbacks_count = SerializerMethodField() discount_price = DecimalField( - max_digits=12, decimal_places=2, read_only=True, allow_null=True, + max_digits=12, + decimal_places=2, + read_only=True, + allow_null=True, coerce_to_string=False, ) personal_orders_only = SerializerMethodField() diff --git a/engine/core/serializers/simple.py b/engine/core/serializers/simple.py index 945b4717..81ae61d6 100644 --- a/engine/core/serializers/simple.py +++ b/engine/core/serializers/simple.py @@ -147,7 +147,10 @@ class ProductSimpleSerializer(ModelSerializer): feedbacks_count = SerializerMethodField() personal_orders_only = SerializerMethodField() discount_price = DecimalField( - max_digits=12, decimal_places=2, read_only=True, allow_null=True, + max_digits=12, + decimal_places=2, + read_only=True, + allow_null=True, coerce_to_string=False, ) diff --git a/engine/core/views.py b/engine/core/views.py index 6eba249b..85782849 100644 --- a/engine/core/views.py +++ b/engine/core/views.py @@ -144,7 +144,9 @@ class CustomGraphQLView(FileUploadGraphQLView): def _default(obj): if isinstance(obj, Decimal): return float(obj) - raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable") + raise TypeError( + f"Object of type {type(obj).__name__} is not JSON serializable" + ) opts = orjson.OPT_NON_STR_KEYS if pretty or request.GET.get("pretty"):