Features: 1) Introduced search fields with transliteration support across filters and Elasticsearch queries; 2) Simplified name filters in product, category, and brand filters by replacing custom logic with standard icontains; 3) Added process_system_query function with enhanced query capabilities.
Fixes: 1) Corrected inconsistent `QuerySet` type hints in vendors module; 2) Resolved string concatenation issue in `get_uuid_as_path` by prepending the path with "users/". Extra: Updated Elasticsearch weighting factors and SMART_FIELDS configuration for better search relevance; removed unused methods and redundant comments in filters and documents; cleaned up migrations and adjusted query building logic.
This commit is contained in:
parent
cf268c8af3
commit
bb5911abe6
6 changed files with 217 additions and 140 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
105
core/filters.py
105
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"))
|
||||
|
|
@ -120,58 +122,25 @@ class ProductFilter(FilterSet):
|
|||
"order_by",
|
||||
]
|
||||
|
||||
def __init__(self, data=None, queryset=None, *, request=None, prefix=None):
|
||||
super().__init__(data=data, queryset=queryset, request=request, prefix=prefix)
|
||||
ordering_param = self.data.get("order_by", "")
|
||||
if ordering_param:
|
||||
order_fields = [field.strip("-") for field in ordering_param.split(",")]
|
||||
if "rating" in order_fields:
|
||||
feedback_qs = (
|
||||
Feedback.objects.filter(order_product__product_id=OuterRef("pk"))
|
||||
.values("order_product__product_id")
|
||||
.annotate(avg_rating=Avg("rating"))
|
||||
.values("avg_rating")
|
||||
)
|
||||
self.queryset = self.queryset.annotate(
|
||||
rating=Coalesce(
|
||||
Subquery(feedback_qs, output_field=FloatField()),
|
||||
Value(0, output_field=FloatField()),
|
||||
)
|
||||
)
|
||||
if "price" in order_fields:
|
||||
self.queryset = self.queryset.annotate(
|
||||
price_order=Coalesce(
|
||||
Max("stocks__price"),
|
||||
Value(0.0),
|
||||
output_field=FloatField(),
|
||||
)
|
||||
)
|
||||
def search_products(self, queryset: QuerySet[Product], name, value):
|
||||
if not value:
|
||||
return queryset
|
||||
|
||||
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")]
|
||||
uuids = [product.get("uuid") for product in process_query(query=value, indexes=("products",))["products"]]
|
||||
|
||||
if not uuids:
|
||||
return queryset.none()
|
||||
return queryset.filter(uuid__in=uuids)
|
||||
|
||||
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_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 +202,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
|
||||
|
||||
|
|
@ -425,8 +394,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 +422,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 +492,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 +520,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 +540,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):
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
|||
18
core/vendors/__init__.py
vendored
18
core/vendors/__init__.py
vendored
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue