Merge branch 'main' into storefront-nuxt
This commit is contained in:
commit
063123d040
9 changed files with 234 additions and 147 deletions
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
eVibes is an eCommerce backend service built with Django. It’s designed for flexibility, making it ideal for various use
|
eVibes — your store without the extra baggage.
|
||||||
cases and learning Django skills. The project is straightforward to customize, allowing for straightforward editing and
|
Everything works out of the box: storefront, product catalog, cart, and orders.
|
||||||
extension.
|
Minimal complexity, maximum flexibility — install, adjust to your needs, and start selling.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -549,7 +549,7 @@ PRODUCT_SCHEMA = {
|
||||||
**BASE_ERRORS,
|
**BASE_ERRORS,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"seo": extend_schema(
|
"seo_meta": extend_schema(
|
||||||
summary=_("SEO Meta snapshot"),
|
summary=_("SEO Meta snapshot"),
|
||||||
description=_("returns a snapshot of the product's SEO meta data"),
|
description=_("returns a snapshot of the product's SEO meta data"),
|
||||||
parameters=[
|
parameters=[
|
||||||
|
|
|
||||||
|
|
@ -13,27 +13,32 @@ from rest_framework.request import Request
|
||||||
from core.models import Brand, Category, Product
|
from core.models import Brand, Category, Product
|
||||||
|
|
||||||
SMART_FIELDS = [
|
SMART_FIELDS = [
|
||||||
"name^8",
|
"name^6",
|
||||||
"name.ngram^8",
|
"name.ngram^6",
|
||||||
"name.phonetic^6",
|
"name.phonetic^4",
|
||||||
"title^5",
|
"name.translit^5",
|
||||||
"title.ngram^4",
|
"title^4",
|
||||||
"title.phonetic^2",
|
"title.ngram^5",
|
||||||
|
"title.phonetic^3",
|
||||||
|
"title.translit^4",
|
||||||
"description^2",
|
"description^2",
|
||||||
"description.ngram",
|
"description.ngram^3",
|
||||||
"description.phonetic",
|
"description.phonetic^2",
|
||||||
"brand_name^5",
|
"description.translit^3",
|
||||||
|
"brand_name^4",
|
||||||
"brand_name.ngram^3",
|
"brand_name.ngram^3",
|
||||||
"brand_name.auto^4",
|
"brand_name.auto^4",
|
||||||
|
"brand_name.translit^4",
|
||||||
"category_name^3",
|
"category_name^3",
|
||||||
"category_name.ngram^2",
|
"category_name.ngram^3",
|
||||||
"category_name.auto^2",
|
"category_name.auto^3",
|
||||||
"sku^9",
|
"category_name.translit^3",
|
||||||
"sku.ngram^6",
|
"sku^7",
|
||||||
"sku.auto^8",
|
"sku.ngram^5",
|
||||||
"partnumber^10",
|
"sku.auto^6",
|
||||||
"partnumber.ngram^7",
|
"partnumber^8",
|
||||||
"partnumber.auto^9",
|
"partnumber.ngram^6",
|
||||||
|
"partnumber.auto^7",
|
||||||
]
|
]
|
||||||
|
|
||||||
functions = [
|
functions = [
|
||||||
|
|
@ -42,75 +47,80 @@ functions = [
|
||||||
"field_value_factor": {
|
"field_value_factor": {
|
||||||
"field": "brand_priority",
|
"field": "brand_priority",
|
||||||
"modifier": "log1p",
|
"modifier": "log1p",
|
||||||
"factor": 0.2,
|
"factor": 0.15,
|
||||||
"missing": 0,
|
"missing": 0,
|
||||||
},
|
},
|
||||||
"weight": 0.6,
|
"weight": 0.35,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filter": Q("term", **{"_index": "products"}),
|
"filter": Q("term", **{"_index": "products"}),
|
||||||
"field_value_factor": {
|
"field_value_factor": {
|
||||||
"field": "rating",
|
"field": "rating",
|
||||||
"modifier": "log1p",
|
"modifier": "log1p",
|
||||||
"factor": 0.15,
|
"factor": 0.10,
|
||||||
"missing": 0,
|
"missing": 0,
|
||||||
},
|
},
|
||||||
"weight": 0.5,
|
"weight": 0.3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filter": Q("term", **{"_index": "products"}),
|
"filter": Q("term", **{"_index": "products"}),
|
||||||
"field_value_factor": {
|
"field_value_factor": {
|
||||||
"field": "total_orders",
|
"field": "total_orders",
|
||||||
"modifier": "log1p",
|
"modifier": "log1p",
|
||||||
"factor": 0.25,
|
"factor": 0.18,
|
||||||
"missing": 0,
|
"missing": 0,
|
||||||
},
|
},
|
||||||
"weight": 0.7,
|
"weight": 0.4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filter": Q("term", **{"_index": "products"}),
|
"filter": Q("term", **{"_index": "products"}),
|
||||||
"field_value_factor": {
|
"field_value_factor": {
|
||||||
"field": "category_priority",
|
"field": "category_priority",
|
||||||
"modifier": "log1p",
|
"modifier": "log1p",
|
||||||
"factor": 0.2,
|
"factor": 0.15,
|
||||||
"missing": 0,
|
"missing": 0,
|
||||||
},
|
},
|
||||||
"weight": 0.6,
|
"weight": 0.35,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filter": Q("term", **{"_index": "categories"}),
|
"filter": Q("term", **{"_index": "categories"}),
|
||||||
"field_value_factor": {
|
"field_value_factor": {
|
||||||
"field": "priority",
|
"field": "priority",
|
||||||
"modifier": "log1p",
|
"modifier": "log1p",
|
||||||
"factor": 0.25,
|
"factor": 0.18,
|
||||||
"missing": 0,
|
"missing": 0,
|
||||||
},
|
},
|
||||||
"weight": 0.8,
|
"weight": 0.45,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filter": Q("term", **{"_index": "brands"}),
|
"filter": Q("term", **{"_index": "brands"}),
|
||||||
"field_value_factor": {
|
"field_value_factor": {
|
||||||
"field": "priority",
|
"field": "priority",
|
||||||
"modifier": "log1p",
|
"modifier": "log1p",
|
||||||
"factor": 0.25,
|
"factor": 0.18,
|
||||||
"missing": 0,
|
"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:
|
if not query:
|
||||||
raise ValueError(_("no search term provided."))
|
raise ValueError(_("no search term provided."))
|
||||||
|
|
||||||
query = query.strip()
|
query = query.strip()
|
||||||
try:
|
try:
|
||||||
exact_shoulds = [
|
exact_shoulds = [
|
||||||
Q("term", **{"name.raw": {"value": query, "boost": 3.0}}),
|
Q("term", **{"name.raw": {"value": query, "boost": 2.0}}),
|
||||||
Q("term", **{"slug": {"value": slugify(query), "boost": 2.0}}),
|
Q("term", **{"slug": {"value": slugify(query), "boost": 1.5}}),
|
||||||
Q("term", **{"sku.raw": {"value": query.lower(), "boost": 8.0}}),
|
Q("term", **{"sku.raw": {"value": query.lower(), "boost": 6.0}}),
|
||||||
Q("term", **{"partnumber.raw": {"value": query.lower(), "boost": 9.0}}),
|
Q("term", **{"partnumber.raw": {"value": query.lower(), "boost": 7.0}}),
|
||||||
]
|
]
|
||||||
|
|
||||||
lang = ""
|
lang = ""
|
||||||
|
|
@ -122,10 +132,16 @@ def process_query(query: str = "", request: Request | None = None) -> dict[str,
|
||||||
is_rtl_or_indic = base in {"ar", "hi"}
|
is_rtl_or_indic = base in {"ar", "hi"}
|
||||||
|
|
||||||
fields_all = SMART_FIELDS[:]
|
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:
|
if is_cjk or is_rtl_or_indic:
|
||||||
fields_all = [f for f in fields_all if ".phonetic" not in f]
|
fields_all = [f for f in fields_all if ".phonetic" not in f]
|
||||||
fields_all = [
|
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"
|
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,
|
query=query,
|
||||||
fields=fields_all,
|
fields=fields_all,
|
||||||
operator="and",
|
operator="and",
|
||||||
|
type="most_fields",
|
||||||
|
tie_breaker=0.2,
|
||||||
**({"fuzziness": fuzzy} if fuzzy else {}),
|
**({"fuzziness": fuzzy} if fuzzy else {}),
|
||||||
),
|
),
|
||||||
Q(
|
Q(
|
||||||
|
|
@ -151,10 +169,10 @@ def process_query(query: str = "", request: Request | None = None) -> dict[str,
|
||||||
if is_code_like:
|
if is_code_like:
|
||||||
text_shoulds.extend(
|
text_shoulds.extend(
|
||||||
[
|
[
|
||||||
Q("term", **{"sku.raw": {"value": query.lower(), "boost": 12.0}}),
|
Q("term", **{"sku.raw": {"value": query.lower(), "boost": 10.0}}),
|
||||||
Q("term", **{"partnumber.raw": {"value": query.lower(), "boost": 14.0}}),
|
Q("term", **{"partnumber.raw": {"value": query.lower(), "boost": 12.0}}),
|
||||||
Q("prefix", **{"sku.raw": {"value": query.lower(), "boost": 6.0}}),
|
Q("prefix", **{"sku.raw": {"value": query.lower(), "boost": 5.0}}),
|
||||||
Q("prefix", **{"partnumber.raw": {"value": query.lower(), "boost": 7.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,
|
minimum_should_match=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
def build_search(indexes, size):
|
def build_search(idxs, size):
|
||||||
return (
|
return (
|
||||||
Search(index=indexes)
|
Search(index=idxs)
|
||||||
.query(query_base)
|
.query(query_base)
|
||||||
.extra(
|
.extra(
|
||||||
rescore={
|
rescore={
|
||||||
|
|
@ -178,29 +196,40 @@ def process_query(query: str = "", request: Request | None = None) -> dict[str,
|
||||||
functions=functions,
|
functions=functions,
|
||||||
boost_mode="sum",
|
boost_mode="sum",
|
||||||
score_mode="sum",
|
score_mode="sum",
|
||||||
max_boost=2.0,
|
max_boost=1.2,
|
||||||
).to_dict(),
|
).to_dict(),
|
||||||
"query_weight": 1.0,
|
"query_weight": 1.0,
|
||||||
"rescore_query_weight": 1.0,
|
"rescore_query_weight": 0.6,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.extra(size=size, track_total_hits=True)
|
.extra(size=size, track_total_hits=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
resp_cats = None
|
||||||
|
if "categories" in indexes:
|
||||||
search_cats = build_search(["categories"], size=22)
|
search_cats = build_search(["categories"], size=22)
|
||||||
search_brands = build_search(["brands"], size=22)
|
|
||||||
search_products = build_search(["products"], size=44)
|
|
||||||
|
|
||||||
resp_cats = search_cats.execute()
|
resp_cats = search_cats.execute()
|
||||||
|
|
||||||
|
resp_brands = None
|
||||||
|
if "brands" in indexes:
|
||||||
|
search_brands = build_search(["brands"], size=22)
|
||||||
resp_brands = search_brands.execute()
|
resp_brands = search_brands.execute()
|
||||||
|
|
||||||
|
resp_products = None
|
||||||
|
if "products" in indexes:
|
||||||
|
search_products = build_search(["products"], size=44)
|
||||||
resp_products = search_products.execute()
|
resp_products = search_products.execute()
|
||||||
|
|
||||||
results: dict = {"products": [], "categories": [], "brands": [], "posts": []}
|
results: dict = {"products": [], "categories": [], "brands": [], "posts": []}
|
||||||
uuids_by_index: dict[str, list] = {"products": [], "categories": [], "brands": []}
|
uuids_by_index: dict[str, list] = {"products": [], "categories": [], "brands": []}
|
||||||
hit_cache: list = []
|
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)
|
hit_cache.append(h)
|
||||||
if getattr(h, "uuid", None):
|
if getattr(h, "uuid", None):
|
||||||
uuids_by_index.setdefault(h.meta.index, []).append(str(h.uuid))
|
uuids_by_index.setdefault(h.meta.index, []).append(str(h.uuid))
|
||||||
|
|
@ -296,8 +325,6 @@ def _lang_analyzer(lang_code: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
class ActiveOnlyMixin:
|
class ActiveOnlyMixin:
|
||||||
"""QuerySet & indexing helpers, so only *active* objects are indexed."""
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().filter(is_active=True)
|
return super().get_queryset().filter(is_active=True)
|
||||||
|
|
||||||
|
|
@ -317,6 +344,9 @@ COMMON_ANALYSIS = {
|
||||||
"double_metaphone": {"type": "phonetic", "encoder": "double_metaphone", "replace": False},
|
"double_metaphone": {"type": "phonetic", "encoder": "double_metaphone", "replace": False},
|
||||||
"arabic_norm": {"type": "arabic_normalization"},
|
"arabic_norm": {"type": "arabic_normalization"},
|
||||||
"indic_norm": {"type": "indic_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": {
|
"analyzer": {
|
||||||
"icu_query": {
|
"icu_query": {
|
||||||
|
|
@ -367,6 +397,32 @@ COMMON_ANALYSIS = {
|
||||||
"tokenizer": "icu_tokenizer",
|
"tokenizer": "icu_tokenizer",
|
||||||
"filter": ["lowercase", "icu_folding", "indic_norm"],
|
"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": {
|
"normalizer": {
|
||||||
"lc_norm": {
|
"lc_norm": {
|
||||||
|
|
@ -378,12 +434,8 @@ COMMON_ANALYSIS = {
|
||||||
|
|
||||||
|
|
||||||
def add_multilang_fields(cls):
|
def add_multilang_fields(cls):
|
||||||
"""
|
|
||||||
Dynamically add multilingual name/description fields and prepare methods to guard against None.
|
|
||||||
"""
|
|
||||||
for code, _lang in settings.LANGUAGES:
|
for code, _lang in settings.LANGUAGES:
|
||||||
lc = code.replace("-", "_").lower()
|
lc = code.replace("-", "_").lower()
|
||||||
# name_{lc}
|
|
||||||
name_field = f"name_{lc}"
|
name_field = f"name_{lc}"
|
||||||
setattr(
|
setattr(
|
||||||
cls,
|
cls,
|
||||||
|
|
@ -396,17 +448,16 @@ def add_multilang_fields(cls):
|
||||||
"raw": fields.KeywordField(ignore_above=256),
|
"raw": fields.KeywordField(ignore_above=256),
|
||||||
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
|
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
|
||||||
"phonetic": fields.TextField(analyzer="name_phonetic"),
|
"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):
|
def make_prepare(attr):
|
||||||
return lambda self, instance: getattr(instance, attr, "") or ""
|
return lambda self, instance: getattr(instance, attr, "") or ""
|
||||||
|
|
||||||
setattr(cls, f"prepare_{name_field}", make_prepare(name_field))
|
setattr(cls, f"prepare_{name_field}", make_prepare(name_field))
|
||||||
|
|
||||||
# description_{lc}
|
|
||||||
desc_field = f"description_{lc}"
|
desc_field = f"description_{lc}"
|
||||||
setattr(
|
setattr(
|
||||||
cls,
|
cls,
|
||||||
|
|
@ -419,6 +470,7 @@ def add_multilang_fields(cls):
|
||||||
"raw": fields.KeywordField(ignore_above=256),
|
"raw": fields.KeywordField(ignore_above=256),
|
||||||
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
|
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
|
||||||
"phonetic": fields.TextField(analyzer="name_phonetic"),
|
"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())):
|
for doc in registry.get_documents(set(registry.get_models())):
|
||||||
qs = doc().get_indexing_queryset()
|
qs = doc().get_indexing_queryset()
|
||||||
doc().update(qs, parallel=True, refresh=True)
|
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"),
|
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
|
||||||
"phonetic": fields.TextField(analyzer="name_phonetic"),
|
"phonetic": fields.TextField(analyzer="name_phonetic"),
|
||||||
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
|
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
|
||||||
|
"translit": fields.TextField(analyzer="translit_index", search_analyzer="translit_query"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
description = fields.TextField(
|
description = fields.TextField(
|
||||||
|
|
@ -25,6 +26,7 @@ class BaseDocument(Document):
|
||||||
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
|
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
|
||||||
"phonetic": fields.TextField(analyzer="name_phonetic"),
|
"phonetic": fields.TextField(analyzer="name_phonetic"),
|
||||||
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
|
"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)
|
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"),
|
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
|
||||||
"phonetic": fields.TextField(analyzer="name_phonetic"),
|
"phonetic": fields.TextField(analyzer="name_phonetic"),
|
||||||
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
|
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
|
||||||
|
"translit": fields.TextField(analyzer="translit_index", search_analyzer="translit_query"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
category_name = fields.TextField(
|
category_name = fields.TextField(
|
||||||
|
|
@ -76,6 +79,7 @@ class ProductDocument(ActiveOnlyMixin, BaseDocument):
|
||||||
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
|
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
|
||||||
"phonetic": fields.TextField(analyzer="name_phonetic"),
|
"phonetic": fields.TextField(analyzer="name_phonetic"),
|
||||||
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
|
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
|
||||||
|
"translit": fields.TextField(analyzer="translit_index", search_analyzer="translit_query"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
102
core/filters.py
102
core/filters.py
|
|
@ -5,18 +5,19 @@ import uuid
|
||||||
from django.core.exceptions import BadRequest
|
from django.core.exceptions import BadRequest
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Avg,
|
Avg,
|
||||||
|
BooleanField,
|
||||||
Case,
|
Case,
|
||||||
Exists,
|
Exists,
|
||||||
FloatField,
|
FloatField,
|
||||||
IntegerField,
|
IntegerField,
|
||||||
|
Max,
|
||||||
OuterRef,
|
OuterRef,
|
||||||
|
Prefetch,
|
||||||
Q,
|
Q,
|
||||||
|
QuerySet,
|
||||||
Subquery,
|
Subquery,
|
||||||
Value,
|
Value,
|
||||||
When,
|
When,
|
||||||
Max,
|
|
||||||
Prefetch,
|
|
||||||
BooleanField,
|
|
||||||
)
|
)
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils.http import urlsafe_base64_decode
|
from django.utils.http import urlsafe_base64_decode
|
||||||
|
|
@ -61,8 +62,9 @@ class CaseInsensitiveListFilter(BaseInFilter, CharFilter):
|
||||||
|
|
||||||
|
|
||||||
class ProductFilter(FilterSet):
|
class ProductFilter(FilterSet):
|
||||||
|
search = CharFilter(field_name="name", method="search_products", label=_("Search"))
|
||||||
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID"))
|
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"))
|
categories = CaseInsensitiveListFilter(field_name="category__name", label=_("Categories"))
|
||||||
category_uuid = CharFilter(method="filter_category", label="Category (UUID)")
|
category_uuid = CharFilter(method="filter_category", label="Category (UUID)")
|
||||||
categories_slugs = CaseInsensitiveListFilter(field_name="category__slug", label=_("Categories Slugs"))
|
categories_slugs = CaseInsensitiveListFilter(field_name="category__slug", label=_("Categories Slugs"))
|
||||||
|
|
@ -147,31 +149,25 @@ class ProductFilter(FilterSet):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_name(self, queryset, _name, value):
|
def search_products(self, queryset: QuerySet[Product], name, value):
|
||||||
search_results = process_query(query=value, request=self.request)["products"]
|
if not value:
|
||||||
uuids = [res["uuid"] for res in search_results if res.get("uuid")]
|
return queryset
|
||||||
|
|
||||||
if not uuids:
|
uuids = [product.get("uuid") for product in process_query(query=value, indexes=("products",))["products"]]
|
||||||
return queryset.none()
|
|
||||||
|
|
||||||
ordering = Case(
|
return queryset.filter(uuid__in=uuids)
|
||||||
*[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, name, value):
|
||||||
|
|
||||||
def filter_include_flag(self, queryset, **_kwargs):
|
|
||||||
if not self.data.get("category_uuid"):
|
if not self.data.get("category_uuid"):
|
||||||
raise BadRequest(_("there must be a category_uuid to use include_subcategories flag"))
|
raise BadRequest(_("there must be a category_uuid to use include_subcategories flag"))
|
||||||
return queryset
|
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):
|
if self.data.get("include_personal_ordered", False):
|
||||||
queryset = queryset.filter(stocks__isnull=False, stocks__quantity__gt=0, stocks__price__gt=0)
|
queryset = queryset.filter(stocks__isnull=False, stocks__quantity__gt=0, stocks__price__gt=0)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def filter_attributes(self, queryset, _name, value):
|
def filter_attributes(self, queryset, name, value):
|
||||||
if not value:
|
if not value:
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
@ -233,7 +229,7 @@ class ProductFilter(FilterSet):
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def filter_category(self, queryset, _name, value):
|
def filter_category(self, queryset, name, value):
|
||||||
if not value:
|
if not value:
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
@ -280,30 +276,6 @@ class ProductFilter(FilterSet):
|
||||||
qs = super().qs
|
qs = super().qs
|
||||||
|
|
||||||
ordering_param = self.data.get("order_by", "")
|
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(
|
qs = qs.annotate(
|
||||||
has_stock=Max(
|
has_stock=Max(
|
||||||
|
|
@ -425,8 +397,9 @@ class WishlistFilter(FilterSet):
|
||||||
|
|
||||||
|
|
||||||
class CategoryFilter(FilterSet):
|
class CategoryFilter(FilterSet):
|
||||||
|
search = CharFilter(field_name="name", method="search_categories", label=_("Search"))
|
||||||
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
|
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"))
|
parent_uuid = CharFilter(method="filter_parent_uuid", label=_("Parent"))
|
||||||
slug = CharFilter(field_name="slug", lookup_expr="exact", label=_("Slug"))
|
slug = CharFilter(field_name="slug", lookup_expr="exact", label=_("Slug"))
|
||||||
whole = BooleanFilter(
|
whole = BooleanFilter(
|
||||||
|
|
@ -452,6 +425,14 @@ class CategoryFilter(FilterSet):
|
||||||
"whole",
|
"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):
|
def filter_order_by(self, queryset, _name, value):
|
||||||
if not value:
|
if not value:
|
||||||
return queryset
|
return queryset
|
||||||
|
|
@ -514,20 +495,6 @@ class CategoryFilter(FilterSet):
|
||||||
|
|
||||||
return qs
|
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):
|
def filter_whole_categories(self, queryset, _name, value):
|
||||||
has_own_products = Exists(Product.objects.filter(category=OuterRef("pk")))
|
has_own_products = Exists(Product.objects.filter(category=OuterRef("pk")))
|
||||||
has_desc_products = Exists(
|
has_desc_products = Exists(
|
||||||
|
|
@ -556,8 +523,9 @@ class CategoryFilter(FilterSet):
|
||||||
|
|
||||||
|
|
||||||
class BrandFilter(FilterSet):
|
class BrandFilter(FilterSet):
|
||||||
|
search = CharFilter(field_name="name", method="search_brands", label=_("Search"))
|
||||||
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
|
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"))
|
slug = CharFilter(field_name="slug", lookup_expr="exact", label=_("Slug"))
|
||||||
categories = CaseInsensitiveListFilter(field_name="categories__uuid", lookup_expr="exact", label=_("Categories"))
|
categories = CaseInsensitiveListFilter(field_name="categories__uuid", lookup_expr="exact", label=_("Categories"))
|
||||||
|
|
||||||
|
|
@ -575,19 +543,13 @@ class BrandFilter(FilterSet):
|
||||||
model = Brand
|
model = Brand
|
||||||
fields = ["uuid", "name", "slug", "priority"]
|
fields = ["uuid", "name", "slug", "priority"]
|
||||||
|
|
||||||
def filter_name(self, queryset, _name, value):
|
def search_brands(self, queryset: QuerySet[Product], name, value):
|
||||||
search_results = process_query(query=value, request=self.request)["brands"]
|
if not value:
|
||||||
uuids = [res["uuid"] for res in search_results if res.get("uuid")]
|
return queryset
|
||||||
|
|
||||||
if not uuids:
|
uuids = [brand.get("uuid") for brand in process_query(query=value, indexes=("brands",))["brands"]]
|
||||||
return queryset.none()
|
|
||||||
|
|
||||||
ordering = Case(
|
return queryset.filter(uuid__in=uuids)
|
||||||
*[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")
|
|
||||||
|
|
||||||
|
|
||||||
class FeedbackFilter(FilterSet):
|
class FeedbackFilter(FilterSet):
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("core", "0042_attribute_name_fa_ir_attribute_name_he_il_and_more"),
|
("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 typing import Any
|
||||||
|
|
||||||
from django.db import IntegrityError, transaction
|
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 (
|
from core.models import (
|
||||||
Attribute,
|
Attribute,
|
||||||
AttributeGroup,
|
AttributeGroup,
|
||||||
|
|
@ -174,7 +173,7 @@ class AbstractVendor:
|
||||||
def auto_resolve_category(self, category_name: str = "") -> Category | None:
|
def auto_resolve_category(self, category_name: str = "") -> Category | None:
|
||||||
if category_name:
|
if category_name:
|
||||||
try:
|
try:
|
||||||
search = process_query(category_name)
|
search = process_system_query(query=category_name, indexes=("categories",))
|
||||||
uuid = search["categories"][0]["uuid"] if search else None
|
uuid = search["categories"][0]["uuid"] if search else None
|
||||||
if uuid:
|
if uuid:
|
||||||
return Category.objects.get(uuid=uuid)
|
return Category.objects.get(uuid=uuid)
|
||||||
|
|
@ -192,7 +191,7 @@ class AbstractVendor:
|
||||||
def auto_resolve_brand(self, brand_name: str = "") -> Brand | None:
|
def auto_resolve_brand(self, brand_name: str = "") -> Brand | None:
|
||||||
if brand_name:
|
if brand_name:
|
||||||
try:
|
try:
|
||||||
search = process_query(brand_name)
|
search = process_system_query(query=brand_name, indexes=("brands",))
|
||||||
uuid = search["brands"][0]["uuid"] if search else None
|
uuid = search["brands"][0]["uuid"] if search else None
|
||||||
if uuid:
|
if uuid:
|
||||||
return Brand.objects.get(uuid=uuid)
|
return Brand.objects.get(uuid=uuid)
|
||||||
|
|
@ -283,13 +282,13 @@ class AbstractVendor:
|
||||||
def get_products(self) -> None:
|
def get_products(self) -> None:
|
||||||
pass
|
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)
|
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)
|
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(
|
return AttributeValue.objects.filter(
|
||||||
product__in=self.get_products_queryset(), product__orderproduct__isnull=True
|
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...")
|
raise ValueError(f"Invalid method {method!r} for products update...")
|
||||||
|
|
||||||
def delete_inactives(self, inactivation_method: str = "deactivate", size: int = 5000) -> None:
|
def delete_inactives(self, inactivation_method: str = "deactivate", size: int = 5000) -> None:
|
||||||
|
filter_kwargs: dict[str, Any] = dict()
|
||||||
|
|
||||||
match inactivation_method:
|
match inactivation_method:
|
||||||
case "deactivate":
|
case "deactivate":
|
||||||
filter_kwargs: dict[str, Any] = {"is_active": False}
|
filter_kwargs: dict[str, Any] = {"is_active": False}
|
||||||
|
|
@ -319,6 +320,9 @@ class AbstractVendor:
|
||||||
case _:
|
case _:
|
||||||
raise ValueError(f"Invalid method {inactivation_method!r} for products cleaner...")
|
raise ValueError(f"Invalid method {inactivation_method!r} for products cleaner...")
|
||||||
|
|
||||||
|
if filter_kwargs == {}:
|
||||||
|
raise ValueError("Invalid filter kwargs...")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
products = self.get_products_queryset()
|
products = self.get_products_queryset()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from uuid import UUID
|
||||||
|
|
||||||
from constance import config
|
from constance import config
|
||||||
from django.conf import settings
|
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.http import Http404
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.decorators import method_decorator
|
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.messages import permission_denied_message
|
||||||
from core.utils.nominatim import fetch_address_suggestions
|
from core.utils.nominatim import fetch_address_suggestions
|
||||||
from core.utils.seo_builders import (
|
from core.utils.seo_builders import (
|
||||||
org_schema,
|
brand_schema,
|
||||||
website_schema,
|
|
||||||
breadcrumb_schema,
|
breadcrumb_schema,
|
||||||
product_schema,
|
|
||||||
category_schema,
|
category_schema,
|
||||||
item_list_schema,
|
item_list_schema,
|
||||||
brand_schema,
|
org_schema,
|
||||||
|
product_schema,
|
||||||
|
website_schema,
|
||||||
)
|
)
|
||||||
from evibes.settings import DEBUG
|
from evibes.settings import DEBUG
|
||||||
from payments.serializers import TransactionProcessSerializer
|
from payments.serializers import TransactionProcessSerializer
|
||||||
|
|
@ -536,7 +536,10 @@ class ProductViewSet(EvibesViewSet):
|
||||||
}
|
}
|
||||||
lookup_field = "lookup_value"
|
lookup_field = "lookup_value"
|
||||||
lookup_url_kwarg = "lookup_value"
|
lookup_url_kwarg = "lookup_value"
|
||||||
additional = {"seo_meta": "ALLOW"}
|
additional = {
|
||||||
|
"seo_meta": "ALLOW",
|
||||||
|
"feedbacks": "ALLOW",
|
||||||
|
}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
|
|
@ -565,6 +568,9 @@ class ProductViewSet(EvibesViewSet):
|
||||||
if not obj:
|
if not obj:
|
||||||
obj = queryset.filter(slug=lookup_value).first()
|
obj = queryset.filter(slug=lookup_value).first()
|
||||||
|
|
||||||
|
if not obj:
|
||||||
|
obj = queryset.filter(sku=lookup_value).first()
|
||||||
|
|
||||||
if not obj:
|
if not obj:
|
||||||
name = "Product"
|
name = "Product"
|
||||||
raise Http404(f"{name} does not exist: {lookup_value}")
|
raise Http404(f"{name} does not exist: {lookup_value}")
|
||||||
|
|
@ -951,6 +957,7 @@ class OrderProductViewSet(EvibesViewSet):
|
||||||
"list": OrderProductSimpleSerializer,
|
"list": OrderProductSimpleSerializer,
|
||||||
"do_feedback": DoFeedbackSerializer,
|
"do_feedback": DoFeedbackSerializer,
|
||||||
}
|
}
|
||||||
|
additional = {"do_feedback": "ALLOW"}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ class User(AbstractUser, NiceModel): # type: ignore [django-manager-missing]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_uuid_as_path(self, *args):
|
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"))
|
email = EmailField(_("email"), unique=True, help_text=_("user email address"))
|
||||||
phone_number = CharField(
|
phone_number = CharField(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue