Merge branch 'main' into storefront-nuxt

This commit is contained in:
Egor Pavlovich Gorbunov 2025-09-30 11:38:28 +03:00
commit 063123d040
9 changed files with 234 additions and 147 deletions

View file

@ -2,9 +2,9 @@
![LOGO](core/docs/images/evibes-big.png)
eVibes is an eCommerce backend service built with Django. Its designed for flexibility, making it ideal for various use
cases and learning Django skills. The project is straightforward to customize, allowing for straightforward editing and
extension.
eVibes — your store without the extra baggage.
Everything works out of the box: storefront, product catalog, cart, and orders.
Minimal complexity, maximum flexibility — install, adjust to your needs, and start selling.
## Table of Contents

View file

@ -549,7 +549,7 @@ PRODUCT_SCHEMA = {
**BASE_ERRORS,
},
),
"seo": extend_schema(
"seo_meta": extend_schema(
summary=_("SEO Meta snapshot"),
description=_("returns a snapshot of the product's SEO meta data"),
parameters=[

View file

@ -13,27 +13,32 @@ from rest_framework.request import Request
from core.models import Brand, Category, Product
SMART_FIELDS = [
"name^8",
"name.ngram^8",
"name.phonetic^6",
"title^5",
"title.ngram^4",
"title.phonetic^2",
"name^6",
"name.ngram^6",
"name.phonetic^4",
"name.translit^5",
"title^4",
"title.ngram^5",
"title.phonetic^3",
"title.translit^4",
"description^2",
"description.ngram",
"description.phonetic",
"brand_name^5",
"description.ngram^3",
"description.phonetic^2",
"description.translit^3",
"brand_name^4",
"brand_name.ngram^3",
"brand_name.auto^4",
"brand_name.translit^4",
"category_name^3",
"category_name.ngram^2",
"category_name.auto^2",
"sku^9",
"sku.ngram^6",
"sku.auto^8",
"partnumber^10",
"partnumber.ngram^7",
"partnumber.auto^9",
"category_name.ngram^3",
"category_name.auto^3",
"category_name.translit^3",
"sku^7",
"sku.ngram^5",
"sku.auto^6",
"partnumber^8",
"partnumber.ngram^6",
"partnumber.auto^7",
]
functions = [
@ -42,75 +47,80 @@ functions = [
"field_value_factor": {
"field": "brand_priority",
"modifier": "log1p",
"factor": 0.2,
"factor": 0.15,
"missing": 0,
},
"weight": 0.6,
"weight": 0.35,
},
{
"filter": Q("term", **{"_index": "products"}),
"field_value_factor": {
"field": "rating",
"modifier": "log1p",
"factor": 0.15,
"factor": 0.10,
"missing": 0,
},
"weight": 0.5,
"weight": 0.3,
},
{
"filter": Q("term", **{"_index": "products"}),
"field_value_factor": {
"field": "total_orders",
"modifier": "log1p",
"factor": 0.25,
"factor": 0.18,
"missing": 0,
},
"weight": 0.7,
"weight": 0.4,
},
{
"filter": Q("term", **{"_index": "products"}),
"field_value_factor": {
"field": "category_priority",
"modifier": "log1p",
"factor": 0.2,
"factor": 0.15,
"missing": 0,
},
"weight": 0.6,
"weight": 0.35,
},
{
"filter": Q("term", **{"_index": "categories"}),
"field_value_factor": {
"field": "priority",
"modifier": "log1p",
"factor": 0.25,
"factor": 0.18,
"missing": 0,
},
"weight": 0.8,
"weight": 0.45,
},
{
"filter": Q("term", **{"_index": "brands"}),
"field_value_factor": {
"field": "priority",
"modifier": "log1p",
"factor": 0.25,
"factor": 0.18,
"missing": 0,
},
"weight": 0.8,
"weight": 0.45,
},
]
def process_query(query: str = "", request: Request | None = None) -> dict[str, list[dict]] | None:
def process_query(
query: str = "",
request: Request | None = None,
indexes: tuple[str, ...] = ("categories", "brands", "products"),
use_transliteration: bool = True,
) -> dict[str, list[dict]] | None:
if not query:
raise ValueError(_("no search term provided."))
query = query.strip()
try:
exact_shoulds = [
Q("term", **{"name.raw": {"value": query, "boost": 3.0}}),
Q("term", **{"slug": {"value": slugify(query), "boost": 2.0}}),
Q("term", **{"sku.raw": {"value": query.lower(), "boost": 8.0}}),
Q("term", **{"partnumber.raw": {"value": query.lower(), "boost": 9.0}}),
Q("term", **{"name.raw": {"value": query, "boost": 2.0}}),
Q("term", **{"slug": {"value": slugify(query), "boost": 1.5}}),
Q("term", **{"sku.raw": {"value": query.lower(), "boost": 6.0}}),
Q("term", **{"partnumber.raw": {"value": query.lower(), "boost": 7.0}}),
]
lang = ""
@ -122,10 +132,16 @@ def process_query(query: str = "", request: Request | None = None) -> dict[str,
is_rtl_or_indic = base in {"ar", "hi"}
fields_all = SMART_FIELDS[:]
if not use_transliteration:
fields_all = [f for f in fields_all if ".translit" not in f]
if is_cjk or is_rtl_or_indic:
fields_all = [f for f in fields_all if ".phonetic" not in f]
fields_all = [
f.replace("name.ngram^8", "name.ngram^10").replace("title.ngram^4", "title.ngram^6") for f in fields_all
f.replace("name.ngram^6", "name.ngram^8")
.replace("title.ngram^5", "title.ngram^7")
.replace("description.ngram^3", "description.ngram^4")
for f in fields_all
]
fuzzy = None if (is_cjk or is_rtl_or_indic) else "AUTO:5,8"
@ -138,6 +154,8 @@ def process_query(query: str = "", request: Request | None = None) -> dict[str,
query=query,
fields=fields_all,
operator="and",
type="most_fields",
tie_breaker=0.2,
**({"fuzziness": fuzzy} if fuzzy else {}),
),
Q(
@ -151,10 +169,10 @@ def process_query(query: str = "", request: Request | None = None) -> dict[str,
if is_code_like:
text_shoulds.extend(
[
Q("term", **{"sku.raw": {"value": query.lower(), "boost": 12.0}}),
Q("term", **{"partnumber.raw": {"value": query.lower(), "boost": 14.0}}),
Q("prefix", **{"sku.raw": {"value": query.lower(), "boost": 6.0}}),
Q("prefix", **{"partnumber.raw": {"value": query.lower(), "boost": 7.0}}),
Q("term", **{"sku.raw": {"value": query.lower(), "boost": 10.0}}),
Q("term", **{"partnumber.raw": {"value": query.lower(), "boost": 12.0}}),
Q("prefix", **{"sku.raw": {"value": query.lower(), "boost": 5.0}}),
Q("prefix", **{"partnumber.raw": {"value": query.lower(), "boost": 6.0}}),
]
)
@ -164,9 +182,9 @@ def process_query(query: str = "", request: Request | None = None) -> dict[str,
minimum_should_match=1,
)
def build_search(indexes, size):
def build_search(idxs, size):
return (
Search(index=indexes)
Search(index=idxs)
.query(query_base)
.extra(
rescore={
@ -178,29 +196,40 @@ def process_query(query: str = "", request: Request | None = None) -> dict[str,
functions=functions,
boost_mode="sum",
score_mode="sum",
max_boost=2.0,
max_boost=1.2,
).to_dict(),
"query_weight": 1.0,
"rescore_query_weight": 1.0,
"rescore_query_weight": 0.6,
},
}
)
.extra(size=size, track_total_hits=True)
)
search_cats = build_search(["categories"], size=22)
search_brands = build_search(["brands"], size=22)
search_products = build_search(["products"], size=44)
resp_cats = None
if "categories" in indexes:
search_cats = build_search(["categories"], size=22)
resp_cats = search_cats.execute()
resp_cats = search_cats.execute()
resp_brands = search_brands.execute()
resp_products = search_products.execute()
resp_brands = None
if "brands" in indexes:
search_brands = build_search(["brands"], size=22)
resp_brands = search_brands.execute()
resp_products = None
if "products" in indexes:
search_products = build_search(["products"], size=44)
resp_products = search_products.execute()
results: dict = {"products": [], "categories": [], "brands": [], "posts": []}
uuids_by_index: dict[str, list] = {"products": [], "categories": [], "brands": []}
hit_cache: list = []
for h in list(resp_cats.hits[:12]) + list(resp_brands.hits[:12]) + list(resp_products.hits[:26]):
for h in (
list(resp_cats.hits[:12] if resp_cats else [])
+ list(resp_brands.hits[:12] if resp_brands else [])
+ list(resp_products.hits[:26] if resp_products else [])
):
hit_cache.append(h)
if getattr(h, "uuid", None):
uuids_by_index.setdefault(h.meta.index, []).append(str(h.uuid))
@ -296,8 +325,6 @@ def _lang_analyzer(lang_code: str) -> str:
class ActiveOnlyMixin:
"""QuerySet & indexing helpers, so only *active* objects are indexed."""
def get_queryset(self):
return super().get_queryset().filter(is_active=True)
@ -317,6 +344,9 @@ COMMON_ANALYSIS = {
"double_metaphone": {"type": "phonetic", "encoder": "double_metaphone", "replace": False},
"arabic_norm": {"type": "arabic_normalization"},
"indic_norm": {"type": "indic_normalization"},
"icu_any_latin": {"type": "icu_transform", "id": "Any-Latin"},
"icu_latin_ascii": {"type": "icu_transform", "id": "Latin-ASCII"},
"icu_ru_latin_bgn": {"type": "icu_transform", "id": "Russian-Latin/BGN"},
},
"analyzer": {
"icu_query": {
@ -367,6 +397,32 @@ COMMON_ANALYSIS = {
"tokenizer": "icu_tokenizer",
"filter": ["lowercase", "icu_folding", "indic_norm"],
},
"translit_index": {
"type": "custom",
"char_filter": ["icu_nfkc_cf"],
"tokenizer": "icu_tokenizer",
"filter": [
"icu_any_latin",
"icu_ru_latin_bgn",
"icu_latin_ascii",
"lowercase",
"icu_folding",
"double_metaphone",
],
},
"translit_query": {
"type": "custom",
"char_filter": ["icu_nfkc_cf"],
"tokenizer": "icu_tokenizer",
"filter": [
"icu_any_latin",
"icu_ru_latin_bgn",
"icu_latin_ascii",
"lowercase",
"icu_folding",
"double_metaphone",
],
},
},
"normalizer": {
"lc_norm": {
@ -378,12 +434,8 @@ COMMON_ANALYSIS = {
def add_multilang_fields(cls):
"""
Dynamically add multilingual name/description fields and prepare methods to guard against None.
"""
for code, _lang in settings.LANGUAGES:
lc = code.replace("-", "_").lower()
# name_{lc}
name_field = f"name_{lc}"
setattr(
cls,
@ -396,17 +448,16 @@ def add_multilang_fields(cls):
"raw": fields.KeywordField(ignore_above=256),
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
"phonetic": fields.TextField(analyzer="name_phonetic"),
"translit": fields.TextField(analyzer="translit_index", search_analyzer="translit_query"),
},
),
)
# prepare_name_{lc} to ensure no None values
def make_prepare(attr):
return lambda self, instance: getattr(instance, attr, "") or ""
setattr(cls, f"prepare_{name_field}", make_prepare(name_field))
# description_{lc}
desc_field = f"description_{lc}"
setattr(
cls,
@ -419,6 +470,7 @@ def add_multilang_fields(cls):
"raw": fields.KeywordField(ignore_above=256),
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
"phonetic": fields.TextField(analyzer="name_phonetic"),
"translit": fields.TextField(analyzer="translit_index", search_analyzer="translit_query"),
},
),
)
@ -429,3 +481,62 @@ def populate_index():
for doc in registry.get_documents(set(registry.get_models())):
qs = doc().get_indexing_queryset()
doc().update(qs, parallel=True, refresh=True)
def process_system_query(
query: str,
*,
indexes: tuple[str, ...] = ("categories", "brands", "products"),
size_per_index: int = 25,
language_code: str | None = None,
use_transliteration: bool = True,
) -> dict[str, list[dict]]:
if not query:
raise ValueError(_("no search term provided."))
q = query.strip()
base = (language_code or "").split("-")[0].lower() if language_code else ""
is_cjk = base in {"ja", "zh"}
is_rtl_or_indic = base in {"ar", "hi"}
fields_all = [f for f in SMART_FIELDS if not f.startswith(("sku", "partnumber"))]
if not use_transliteration:
fields_all = [f for f in fields_all if ".translit" not in f]
if is_cjk or is_rtl_or_indic:
fields_all = [f for f in fields_all if ".phonetic" not in f]
fields_all = [
f.replace("ngram^6", "ngram^8").replace("ngram^5", "ngram^7").replace("ngram^3", "ngram^4")
for f in fields_all
]
fuzzy = None if (is_cjk or is_rtl_or_indic) else "AUTO:5,8"
mm = Q(
"multi_match",
query=q,
fields=fields_all,
operator="and",
type="most_fields",
tie_breaker=0.2,
**({"fuzziness": fuzzy} if fuzzy else {}),
)
results: dict[str, list[dict]] = {idx: [] for idx in indexes}
for idx in indexes:
s = Search(index=[idx]).query(mm).extra(size=size_per_index, track_total_hits=False)
resp = s.execute()
for h in resp.hits:
name = getattr(h, "name", None) or getattr(h, "title", None) or "N/A"
results[idx].append(
{
"id": getattr(h, "uuid", None) or h.meta.id,
"name": name,
"slug": getattr(h, "slug", ""),
"score": getattr(h.meta, "score", None),
}
)
return results

View file

@ -15,6 +15,7 @@ class BaseDocument(Document):
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
"phonetic": fields.TextField(analyzer="name_phonetic"),
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
"translit": fields.TextField(analyzer="translit_index", search_analyzer="translit_query"),
},
)
description = fields.TextField(
@ -25,6 +26,7 @@ class BaseDocument(Document):
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
"phonetic": fields.TextField(analyzer="name_phonetic"),
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
"translit": fields.TextField(analyzer="translit_index", search_analyzer="translit_query"),
},
)
slug = fields.KeywordField(attr="slug", index=False)
@ -66,6 +68,7 @@ class ProductDocument(ActiveOnlyMixin, BaseDocument):
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
"phonetic": fields.TextField(analyzer="name_phonetic"),
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
"translit": fields.TextField(analyzer="translit_index", search_analyzer="translit_query"),
},
)
category_name = fields.TextField(
@ -76,6 +79,7 @@ class ProductDocument(ActiveOnlyMixin, BaseDocument):
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
"phonetic": fields.TextField(analyzer="name_phonetic"),
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
"translit": fields.TextField(analyzer="translit_index", search_analyzer="translit_query"),
},
)

View file

@ -5,18 +5,19 @@ import uuid
from django.core.exceptions import BadRequest
from django.db.models import (
Avg,
BooleanField,
Case,
Exists,
FloatField,
IntegerField,
Max,
OuterRef,
Prefetch,
Q,
QuerySet,
Subquery,
Value,
When,
Max,
Prefetch,
BooleanField,
)
from django.db.models.functions import Coalesce
from django.utils.http import urlsafe_base64_decode
@ -61,8 +62,9 @@ class CaseInsensitiveListFilter(BaseInFilter, CharFilter):
class ProductFilter(FilterSet):
search = CharFilter(field_name="name", method="search_products", label=_("Search"))
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID"))
name = CharFilter(method="filter_name", label=_("Name"))
name = CharFilter(lookup_expr="icontains", label=_("Name"))
categories = CaseInsensitiveListFilter(field_name="category__name", label=_("Categories"))
category_uuid = CharFilter(method="filter_category", label="Category (UUID)")
categories_slugs = CaseInsensitiveListFilter(field_name="category__slug", label=_("Categories Slugs"))
@ -147,31 +149,25 @@ class ProductFilter(FilterSet):
)
)
def filter_name(self, queryset, _name, value):
search_results = process_query(query=value, request=self.request)["products"]
uuids = [res["uuid"] for res in search_results if res.get("uuid")]
def search_products(self, queryset: QuerySet[Product], name, value):
if not value:
return queryset
if not uuids:
return queryset.none()
uuids = [product.get("uuid") for product in process_query(query=value, indexes=("products",))["products"]]
ordering = Case(
*[When(uuid=uuid_, then=pos) for pos, uuid_ in enumerate(uuids)],
output_field=IntegerField(),
)
return queryset.filter(uuid__in=uuids)
return queryset.filter(uuid__in=uuids).annotate(_order=ordering).order_by("_order")
def filter_include_flag(self, queryset, **_kwargs):
def filter_include_flag(self, queryset, name, value):
if not self.data.get("category_uuid"):
raise BadRequest(_("there must be a category_uuid to use include_subcategories flag"))
return queryset
def filter_include_personal_ordered(self, queryset, **_kwargs):
def filter_include_personal_ordered(self, queryset, name, value):
if self.data.get("include_personal_ordered", False):
queryset = queryset.filter(stocks__isnull=False, stocks__quantity__gt=0, stocks__price__gt=0)
return queryset
def filter_attributes(self, queryset, _name, value):
def filter_attributes(self, queryset, name, value):
if not value:
return queryset
@ -233,7 +229,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
@ -280,30 +276,6 @@ class ProductFilter(FilterSet):
qs = super().qs
ordering_param = self.data.get("order_by", "")
if ordering_param:
order_fields_no_sign = [field.strip("-") for field in ordering_param.split(",")]
if "rating" in order_fields_no_sign:
feedback_qs = (
Feedback.objects.filter(order_product__product_id=OuterRef("pk"))
.values("order_product__product_id")
.annotate(avg_rating=Avg("rating"))
.values("avg_rating")
)
qs = qs.annotate(
rating=Coalesce(
Subquery(feedback_qs, output_field=FloatField()),
Value(0, output_field=FloatField()),
)
)
if ordering_param and any(f.lstrip("-") == "price" for f in ordering_param.split(",")):
qs = qs.annotate(
price_order=Coalesce(
Max("stocks__price"),
Value(0.0),
output_field=FloatField(),
)
)
qs = qs.annotate(
has_stock=Max(
@ -425,8 +397,9 @@ class WishlistFilter(FilterSet):
class CategoryFilter(FilterSet):
search = CharFilter(field_name="name", method="search_categories", label=_("Search"))
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
name = CharFilter(method="filter_name", label=_("Name"))
name = CharFilter(lookup_expr="icontains", label=_("Name"))
parent_uuid = CharFilter(method="filter_parent_uuid", label=_("Parent"))
slug = CharFilter(field_name="slug", lookup_expr="exact", label=_("Slug"))
whole = BooleanFilter(
@ -452,6 +425,14 @@ class CategoryFilter(FilterSet):
"whole",
]
def search_categories(self, queryset: QuerySet[Product], name, value):
if not value:
return queryset
uuids = [category.get("uuid") for category in process_query(query=value, indexes=("categories",))["categories"]]
return queryset.filter(uuid__in=uuids)
def filter_order_by(self, queryset, _name, value):
if not value:
return queryset
@ -514,20 +495,6 @@ class CategoryFilter(FilterSet):
return qs
def filter_name(self, queryset, _name, value):
search_results = process_query(query=value, request=self.request)["categories"]
uuids = [res["uuid"] for res in search_results if res.get("uuid")]
if not uuids:
return queryset.none()
ordering = Case(
*[When(uuid=uuid_, then=pos) for pos, uuid_ in enumerate(uuids)],
output_field=IntegerField(),
)
return queryset.filter(uuid__in=uuids).annotate(_order=ordering).order_by("_order")
def filter_whole_categories(self, queryset, _name, value):
has_own_products = Exists(Product.objects.filter(category=OuterRef("pk")))
has_desc_products = Exists(
@ -556,8 +523,9 @@ class CategoryFilter(FilterSet):
class BrandFilter(FilterSet):
search = CharFilter(field_name="name", method="search_brands", label=_("Search"))
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
name = CharFilter(method="filter_name", label=_("Name"))
name = CharFilter(lookup_expr="icontains", label=_("Name"))
slug = CharFilter(field_name="slug", lookup_expr="exact", label=_("Slug"))
categories = CaseInsensitiveListFilter(field_name="categories__uuid", lookup_expr="exact", label=_("Categories"))
@ -575,19 +543,13 @@ class BrandFilter(FilterSet):
model = Brand
fields = ["uuid", "name", "slug", "priority"]
def filter_name(self, queryset, _name, value):
search_results = process_query(query=value, request=self.request)["brands"]
uuids = [res["uuid"] for res in search_results if res.get("uuid")]
def search_brands(self, queryset: QuerySet[Product], name, value):
if not value:
return queryset
if not uuids:
return queryset.none()
uuids = [brand.get("uuid") for brand in process_query(query=value, indexes=("brands",))["brands"]]
ordering = Case(
*[When(uuid=uuid_, then=pos) for pos, uuid_ in enumerate(uuids)],
output_field=IntegerField(),
)
return queryset.filter(uuid__in=uuids).annotate(_order=ordering).order_by("_order")
return queryset.filter(uuid__in=uuids)
class FeedbackFilter(FilterSet):

View file

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0042_attribute_name_fa_ir_attribute_name_he_il_and_more"),
]

View file

@ -5,9 +5,8 @@ from math import ceil, log10
from typing import Any
from django.db import IntegrityError, transaction
from django.db.models import QuerySet
from core.elasticsearch import process_query
from core.elasticsearch import process_system_query
from core.models import (
Attribute,
AttributeGroup,
@ -174,7 +173,7 @@ class AbstractVendor:
def auto_resolve_category(self, category_name: str = "") -> Category | None:
if category_name:
try:
search = process_query(category_name)
search = process_system_query(query=category_name, indexes=("categories",))
uuid = search["categories"][0]["uuid"] if search else None
if uuid:
return Category.objects.get(uuid=uuid)
@ -192,7 +191,7 @@ class AbstractVendor:
def auto_resolve_brand(self, brand_name: str = "") -> Brand | None:
if brand_name:
try:
search = process_query(brand_name)
search = process_system_query(query=brand_name, indexes=("brands",))
uuid = search["brands"][0]["uuid"] if search else None
if uuid:
return Brand.objects.get(uuid=uuid)
@ -283,13 +282,13 @@ class AbstractVendor:
def get_products(self) -> None:
pass
def get_products_queryset(self) -> QuerySet[Product] | None:
def get_products_queryset(self):
return Product.objects.filter(stocks__vendor=self.get_vendor_instance(), orderproduct__isnull=True)
def get_stocks_queryset(self) -> QuerySet[Stock] | None:
def get_stocks_queryset(self):
return Stock.objects.filter(product__in=self.get_products_queryset(), product__orderproduct__isnull=True)
def get_attribute_values_queryset(self) -> QuerySet[AttributeValue] | None:
def get_attribute_values_queryset(self):
return AttributeValue.objects.filter(
product__in=self.get_products_queryset(), product__orderproduct__isnull=True
)
@ -311,6 +310,8 @@ class AbstractVendor:
raise ValueError(f"Invalid method {method!r} for products update...")
def delete_inactives(self, inactivation_method: str = "deactivate", size: int = 5000) -> None:
filter_kwargs: dict[str, Any] = dict()
match inactivation_method:
case "deactivate":
filter_kwargs: dict[str, Any] = {"is_active": False}
@ -319,6 +320,9 @@ class AbstractVendor:
case _:
raise ValueError(f"Invalid method {inactivation_method!r} for products cleaner...")
if filter_kwargs == {}:
raise ValueError("Invalid filter kwargs...")
while True:
products = self.get_products_queryset()

View file

@ -4,7 +4,7 @@ from uuid import UUID
from constance import config
from django.conf import settings
from django.db.models import Q, Prefetch
from django.db.models import Prefetch, Q
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
@ -109,13 +109,13 @@ from core.utils import format_attributes
from core.utils.messages import permission_denied_message
from core.utils.nominatim import fetch_address_suggestions
from core.utils.seo_builders import (
org_schema,
website_schema,
brand_schema,
breadcrumb_schema,
product_schema,
category_schema,
item_list_schema,
brand_schema,
org_schema,
product_schema,
website_schema,
)
from evibes.settings import DEBUG
from payments.serializers import TransactionProcessSerializer
@ -536,7 +536,10 @@ class ProductViewSet(EvibesViewSet):
}
lookup_field = "lookup_value"
lookup_url_kwarg = "lookup_value"
additional = {"seo_meta": "ALLOW"}
additional = {
"seo_meta": "ALLOW",
"feedbacks": "ALLOW",
}
def get_queryset(self):
qs = super().get_queryset()
@ -565,6 +568,9 @@ class ProductViewSet(EvibesViewSet):
if not obj:
obj = queryset.filter(slug=lookup_value).first()
if not obj:
obj = queryset.filter(sku=lookup_value).first()
if not obj:
name = "Product"
raise Http404(f"{name} does not exist: {lookup_value}")
@ -951,6 +957,7 @@ class OrderProductViewSet(EvibesViewSet):
"list": OrderProductSimpleSerializer,
"do_feedback": DoFeedbackSerializer,
}
additional = {"do_feedback": "ALLOW"}
def get_queryset(self):
qs = super().get_queryset()

View file

@ -80,7 +80,7 @@ class User(AbstractUser, NiceModel): # type: ignore [django-manager-missing]
"""
def get_uuid_as_path(self, *args):
return str(self.uuid) + "/" + args[0]
return "users/" + str(self.uuid) + "/" + args[0]
email = EmailField(_("email"), unique=True, help_text=_("user email address"))
phone_number = CharField(