Merge branch 'main' into storefront-nuxt

This commit is contained in:
Egor Pavlovich Gorbunov 2025-11-11 15:38:29 +03:00
commit d7f5ed4141
9 changed files with 46 additions and 45 deletions

View file

@ -1,7 +1,7 @@
import logging import logging
import requests import requests
from constance import config from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.db import transaction from django.db import transaction
@ -51,7 +51,7 @@ class AmoCRM:
payload = { payload = {
"client_id": self.client_id, "client_id": self.client_id,
"client_secret": self.client_secret, "client_secret": self.client_secret,
"redirect_uri": f"https://api.{config.BASE_DOMAIN}/", "redirect_uri": f"https://api.{settings.BASE_DOMAIN}/",
} }
if self.refresh_token: if self.refresh_token:
payload["grant_type"] = "refresh_token" payload["grant_type"] = "refresh_token"

View file

@ -3,6 +3,7 @@ from contextlib import suppress
from typing import Any from typing import Any
from constance import config from constance import config
from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Max, Min, QuerySet from django.db.models import Max, Min, QuerySet
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -136,7 +137,7 @@ class BrandType(DjangoObjectType): # type: ignore [misc]
def resolve_seo_meta(self: Brand, info) -> dict[str, str | list[Any] | dict[str, str] | None]: def resolve_seo_meta(self: Brand, info) -> dict[str, str | list[Any] | dict[str, str] | None]:
lang = graphene_current_lang() lang = graphene_current_lang()
base = f"https://{config.BASE_DOMAIN}" base = f"https://{settings.BASE_DOMAIN}"
canonical = f"{base}/{lang}/brand/{self.slug}" canonical = f"{base}/{lang}/brand/{self.slug}"
title = f"{self.name} | {config.PROJECT_NAME}" title = f"{self.name} | {config.PROJECT_NAME}"
description = (self.description or "")[:180] description = (self.description or "")[:180]
@ -234,7 +235,7 @@ class CategoryType(DjangoObjectType): # type: ignore [misc]
return result return result
def resolve_image(self: Category, info) -> str: def resolve_image(self: Category, info) -> str:
return info.context.build_absolute_uri(self.image.url) if self.image else "" return self.image_url
def resolve_markup_percent(self: Category, info) -> float: def resolve_markup_percent(self: Category, info) -> float:
if info.context.user.has_perm("core.view_category"): if info.context.user.has_perm("core.view_category"):
@ -262,7 +263,7 @@ class CategoryType(DjangoObjectType): # type: ignore [misc]
def resolve_seo_meta(self: Category, info): def resolve_seo_meta(self: Category, info):
lang = graphene_current_lang() lang = graphene_current_lang()
base = f"https://{config.BASE_DOMAIN}" base = f"https://{settings.BASE_DOMAIN}"
canonical = f"{base}/{lang}/catalog/{self.slug}" canonical = f"{base}/{lang}/catalog/{self.slug}"
title = f"{self.name} | {config.PROJECT_NAME}" title = f"{self.name} | {config.PROJECT_NAME}"
description = (self.description or "")[:180] description = (self.description or "")[:180]
@ -465,8 +466,8 @@ class ProductImageType(DjangoObjectType): # type: ignore [misc]
filter_fields = ["uuid"] filter_fields = ["uuid"]
description = _("product's images") description = _("product's images")
def resolve_image(self: ProductImage, info): def resolve_image(self: ProductImage, _info):
return info.context.build_absolute_uri(self.image.url) if self.image else "" return self.image_url
class ProductType(DjangoObjectType): # type: ignore [misc] class ProductType(DjangoObjectType): # type: ignore [misc]
@ -534,7 +535,7 @@ class ProductType(DjangoObjectType): # type: ignore [misc]
def resolve_seo_meta(self: Product, info): def resolve_seo_meta(self: Product, info):
lang = graphene_current_lang() lang = graphene_current_lang()
base = f"https://{config.BASE_DOMAIN}" base = f"https://{settings.BASE_DOMAIN}"
canonical = f"{base}/{lang}/product/{self.slug}" canonical = f"{base}/{lang}/product/{self.slug}"
title = f"{self.name} | {config.PROJECT_NAME}" title = f"{self.name} | {config.PROJECT_NAME}"
description = (self.description or "")[:180] description = (self.description or "")[:180]

View file

@ -420,6 +420,13 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel): #
return list(by_attr.values()) # type: ignore [arg-type] return list(by_attr.values()) # type: ignore [arg-type]
@cached_property
def image_url(self) -> str:
with suppress(ValueError):
url = str(self.image.url)
url = url if "http" in url else f"https://api.{settings.BASE_DOMAIN}{url}"
return ""
class Meta: class Meta:
verbose_name = _("category") verbose_name = _("category")
verbose_name_plural = _("categories") verbose_name_plural = _("categories")
@ -843,6 +850,13 @@ class ProductImage(ExportModelOperationsMixin("product_image"), NiceModel): # t
def __str__(self) -> str: def __str__(self) -> str:
return self.alt return self.alt
@cached_property
def image_url(self) -> str:
with suppress(ValueError):
url = str(self.image.url)
url = url if "http" in url else f"https://api.{settings.BASE_DOMAIN}{url}"
return ""
class Meta: class Meta:
ordering = ("priority",) ordering = ("priority",)
verbose_name = _("product image") verbose_name = _("product image")
@ -1953,5 +1967,5 @@ class DigitalAssetDownload(ExportModelOperationsMixin("attribute_group"), NiceMo
@property @property
def url(self): def url(self):
return ( return (
f"https://api.{config.BASE_DOMAIN}/download/{urlsafe_base64_encode(force_bytes(self.order_product.uuid))}" f"https://api.{settings.BASE_DOMAIN}/download/{urlsafe_base64_encode(force_bytes(self.order_product.uuid))}"
) )

View file

@ -56,7 +56,6 @@ class CategoryDetailListSerializer(ListSerializer):
class CategoryDetailSerializer(ModelSerializer): class CategoryDetailSerializer(ModelSerializer):
children = SerializerMethodField() children = SerializerMethodField()
image = SerializerMethodField()
filterable_attributes = SerializerMethodField() filterable_attributes = SerializerMethodField()
class Meta: class Meta:
@ -75,11 +74,6 @@ class CategoryDetailSerializer(ModelSerializer):
"modified", "modified",
] ]
def get_image(self, obj: Category) -> str | None:
with suppress(ValueError):
return obj.image.url
return None
def get_filterable_attributes(self, obj: Category) -> list[FilterableAttribute]: def get_filterable_attributes(self, obj: Category) -> list[FilterableAttribute]:
return obj.filterable_attributes return obj.filterable_attributes
@ -161,8 +155,6 @@ class ProductTagDetailSerializer(ModelSerializer):
class ProductImageDetailSerializer(ModelSerializer): class ProductImageDetailSerializer(ModelSerializer):
image = SerializerMethodField()
class Meta: class Meta:
model = ProductImage model = ProductImage
fields = [ fields = [
@ -174,9 +166,6 @@ class ProductImageDetailSerializer(ModelSerializer):
"modified", "modified",
] ]
def get_image(self, obj: ProductImage) -> str:
return obj.image.url or ""
class AttributeDetailSerializer(ModelSerializer): class AttributeDetailSerializer(ModelSerializer):
categories = CategoryDetailSerializer(many=True) categories = CategoryDetailSerializer(many=True)

View file

@ -42,7 +42,6 @@ class AttributeGroupSimpleSerializer(ModelSerializer): # type: ignore [type-arg
class CategorySimpleSerializer(ModelSerializer): # type: ignore [type-arg] class CategorySimpleSerializer(ModelSerializer): # type: ignore [type-arg]
children = SerializerMethodField() children = SerializerMethodField()
image = SerializerMethodField()
class Meta: class Meta:
model = Category model = Category
@ -54,11 +53,6 @@ class CategorySimpleSerializer(ModelSerializer): # type: ignore [type-arg]
"children", "children",
] ]
def get_image(self, obj: Category) -> str | None:
with suppress(ValueError):
return str(obj.image.url)
return None
def get_children(self, obj: Category) -> dict[str, Any]: def get_children(self, obj: Category) -> dict[str, Any]:
request = self.context.get("request") request = self.context.get("request")
if request is not None and request.user.has_perm("view_category"): if request is not None and request.user.has_perm("view_category"):

View file

@ -1,5 +1,5 @@
from constance import config from django.conf import settings
def get_flag_by_language(language: str) -> str: def get_flag_by_language(language: str) -> str:
return f"https://api.{config.BASE_DOMAIN}/static/flags/{language}.png" return f"https://api.{settings.BASE_DOMAIN}/static/flags/{language}.png"

View file

@ -9,8 +9,8 @@ def org_schema():
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "Organization", "@type": "Organization",
"name": config.COMPANY_NAME, "name": config.COMPANY_NAME,
"url": f"https://{config.BASE_DOMAIN}/", "url": f"https://{settings.BASE_DOMAIN}/",
"logo": f"https://{config.BASE_DOMAIN}/static/logo.png", "logo": f"https://{settings.BASE_DOMAIN}/static/logo.png",
} }
@ -19,10 +19,10 @@ def website_schema():
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "WebSite", "@type": "WebSite",
"name": config.PROJECT_NAME, "name": config.PROJECT_NAME,
"url": f"https://{config.BASE_DOMAIN}/", "url": f"https://{settings.BASE_DOMAIN}/",
"potentialAction": { "potentialAction": {
"@type": "SearchAction", "@type": "SearchAction",
"target": f"https://{config.BASE_DOMAIN}/search?q={{query}}", "target": f"https://{settings.BASE_DOMAIN}/search?q={{query}}",
"query-input": "required name=query", "query-input": "required name=query",
}, },
} }
@ -56,7 +56,7 @@ def product_schema(product, images, rating=None):
"priceCurrency": settings.CURRENCY_CODE, "priceCurrency": settings.CURRENCY_CODE,
"availability": "https://schema.org/InStock" if stock.quantity > 0 else "https://schema.org/OutOfStock", "availability": "https://schema.org/InStock" if stock.quantity > 0 else "https://schema.org/OutOfStock",
"sku": stock.sku, "sku": stock.sku,
"url": f"https://{config.BASE_DOMAIN}/product/{product.slug}", "url": f"https://{settings.BASE_DOMAIN}/product/{product.slug}",
} }
) )
data = { data = {

View file

@ -271,7 +271,7 @@ class CategoryViewSet(EvibesViewSet):
title = f"{category.name} | {config.PROJECT_NAME}" title = f"{category.name} | {config.PROJECT_NAME}"
description = (category.description or "")[:180] description = (category.description or "")[:180]
canonical = f"https://{config.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{category.slug}" canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{category.slug}"
og_image = request.build_absolute_uri(category.image.url) if getattr(category, "image", None) else "" og_image = request.build_absolute_uri(category.image.url) if getattr(category, "image", None) else ""
og = { og = {
@ -283,10 +283,10 @@ class CategoryViewSet(EvibesViewSet):
} }
tw = {"card": "summary_large_image", "title": title, "description": description} tw = {"card": "summary_large_image", "title": title, "description": description}
crumbs = [("Home", f"https://{config.BASE_DOMAIN}/")] crumbs = [("Home", f"https://{settings.BASE_DOMAIN}/")]
if category.get_ancestors().exists(): if category.get_ancestors().exists():
for c in category.get_ancestors(): for c in category.get_ancestors():
crumbs.append((c.name, f"https://{config.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{c.slug}")) crumbs.append((c.name, f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{c.slug}"))
crumbs.append((category.name, canonical)) crumbs.append((category.name, canonical))
json_ld = [org_schema(), website_schema(), breadcrumb_schema(crumbs), category_schema(category, canonical)] json_ld = [org_schema(), website_schema(), breadcrumb_schema(crumbs), category_schema(category, canonical)]
@ -303,7 +303,7 @@ class CategoryViewSet(EvibesViewSet):
.distinct()[:24] .distinct()[:24]
) )
for p in qs: for p in qs:
product_urls.append(f"https://{config.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/product/{p.slug}") product_urls.append(f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/product/{p.slug}")
if product_urls: if product_urls:
json_ld.append(item_list_schema(product_urls)) json_ld.append(item_list_schema(product_urls))
@ -388,7 +388,7 @@ class BrandViewSet(EvibesViewSet):
title = f"{brand.name} | {config.PROJECT_NAME}" title = f"{brand.name} | {config.PROJECT_NAME}"
description = (brand.description or "")[:180] description = (brand.description or "")[:180]
canonical = f"https://{config.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/brand/{brand.slug}" canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/brand/{brand.slug}"
logo_url = ( logo_url = (
request.build_absolute_uri(brand.big_logo.url) request.build_absolute_uri(brand.big_logo.url)
@ -408,7 +408,7 @@ class BrandViewSet(EvibesViewSet):
tw = {"card": "summary_large_image", "title": title, "description": description} tw = {"card": "summary_large_image", "title": title, "description": description}
crumbs = [ crumbs = [
("Home", f"https://{config.BASE_DOMAIN}/"), ("Home", f"https://{settings.BASE_DOMAIN}/"),
(brand.name, canonical), (brand.name, canonical),
] ]
@ -528,7 +528,7 @@ class ProductViewSet(EvibesViewSet):
rating = {"value": p.rating, "count": p.feedbacks_count} rating = {"value": p.rating, "count": p.feedbacks_count}
title = f"{p.name} | {config.PROJECT_NAME}" title = f"{p.name} | {config.PROJECT_NAME}"
description = (p.description or "")[:180] description = (p.description or "")[:180]
canonical = f"https://{config.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/product/{p.slug}" canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/product/{p.slug}"
og = { og = {
"title": title, "title": title,
"description": description, "description": description,
@ -538,10 +538,10 @@ class ProductViewSet(EvibesViewSet):
} }
tw = {"card": "summary_large_image", "title": title, "description": description} tw = {"card": "summary_large_image", "title": title, "description": description}
crumbs = [("Home", f"https://{config.BASE_DOMAIN}/")] crumbs = [("Home", f"https://{settings.BASE_DOMAIN}/")]
if p.category: if p.category:
for c in p.category.get_ancestors(include_self=True): for c in p.category.get_ancestors(include_self=True):
crumbs.append((c.name, f"https://{config.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{c.slug}")) crumbs.append((c.name, f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{c.slug}"))
crumbs.append((p.name, canonical)) crumbs.append((p.name, canonical))
json_ld = [org_schema(), website_schema()] json_ld = [org_schema(), website_schema()]

View file

@ -7,13 +7,16 @@ from typing import Any
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
EVIBES_VERSION = "2025.4" EVIBES_VERSION = "2025.4"
RELEASE_DATE = datetime(2025, 9, 13) RELEASE_DATE = datetime(2025, 11, 9)
BASE_DIR = Path(__file__).resolve().parent.parent.parent BASE_DIR = Path(__file__).resolve().parent.parent.parent
INITIALIZED = (BASE_DIR / ".initialized").exists()
SECRET_KEY = getenv("SECRET_KEY", "SUPER_SECRET_KEY") SECRET_KEY = getenv("SECRET_KEY", "SUPER_SECRET_KEY")
DEBUG = bool(int(getenv("DEBUG", "1"))) DEBUG = bool(int(getenv("DEBUG", "1")))
BASE_DOMAIN: str = getenv("EVIBES_BASE_DOMAIN", "localhost")
ALLOWED_HOSTS: set[str] = { ALLOWED_HOSTS: set[str] = {
"app", "app",
"worker", "worker",
@ -294,10 +297,10 @@ TIME_ZONE: str = getenv("TIME_ZONE", "Europe/London")
WHITENOISE_MANIFEST_STRICT: bool = False WHITENOISE_MANIFEST_STRICT: bool = False
STATIC_URL: str = "/static/" STATIC_URL: str = f"https://api.{BASE_DOMAIN}/static/" if INITIALIZED else "static/"
STATIC_ROOT: Path = BASE_DIR / "static" STATIC_ROOT: Path = BASE_DIR / "static"
MEDIA_URL: str = "/media/" MEDIA_URL: str = f"https://api.{BASE_DOMAIN}/media/" if INITIALIZED else "media/"
MEDIA_ROOT: Path = BASE_DIR / "media" MEDIA_ROOT: Path = BASE_DIR / "media"
AUTH_USER_MODEL: str = "vibes_auth.User" AUTH_USER_MODEL: str = "vibes_auth.User"