diff --git a/README.md b/README.md index 5eacae06..38a682c7 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ ![LOGO](core/docs/images/evibes-big.png) -eVibes is an eCommerce backend service built with Django. It’s 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 diff --git a/core/docs/drf/viewsets.py b/core/docs/drf/viewsets.py index 83a9e713..b42c2d1e 100644 --- a/core/docs/drf/viewsets.py +++ b/core/docs/drf/viewsets.py @@ -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=[ diff --git a/core/elasticsearch/__init__.py b/core/elasticsearch/__init__.py index daea16d6..9316ffdd 100644 --- a/core/elasticsearch/__init__.py +++ b/core/elasticsearch/__init__.py @@ -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 diff --git a/core/elasticsearch/documents.py b/core/elasticsearch/documents.py index 008608ba..4e1bb8e9 100644 --- a/core/elasticsearch/documents.py +++ b/core/elasticsearch/documents.py @@ -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"), }, ) diff --git a/core/filters.py b/core/filters.py index d9706a30..3ac968a1 100644 --- a/core/filters.py +++ b/core/filters.py @@ -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): diff --git a/core/migrations/0043_attribute_is_filterable_and_more.py b/core/migrations/0043_attribute_is_filterable_and_more.py index cac31e0c..305d2d49 100644 --- a/core/migrations/0043_attribute_is_filterable_and_more.py +++ b/core/migrations/0043_attribute_is_filterable_and_more.py @@ -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"), ] diff --git a/core/vendors/__init__.py b/core/vendors/__init__.py index 781a5b40..634369fc 100644 --- a/core/vendors/__init__.py +++ b/core/vendors/__init__.py @@ -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() diff --git a/core/viewsets.py b/core/viewsets.py index cc802cc8..b9cefb52 100644 --- a/core/viewsets.py +++ b/core/viewsets.py @@ -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() diff --git a/vibes_auth/models.py b/vibes_auth/models.py index fe7f0dda..2133a8ab 100644 --- a/vibes_auth/models.py +++ b/vibes_auth/models.py @@ -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(