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:
Egor Pavlovich Gorbunov 2025-09-27 15:56:52 +03:00
parent cf268c8af3
commit bb5911abe6
6 changed files with 217 additions and 140 deletions

View file

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

View file

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

View file

@ -5,18 +5,19 @@ import uuid
from django.core.exceptions import BadRequest
from django.db.models import (
Avg,
BooleanField,
Case,
Exists,
FloatField,
IntegerField,
Max,
OuterRef,
Prefetch,
Q,
QuerySet,
Subquery,
Value,
When,
Max,
Prefetch,
BooleanField,
)
from django.db.models.functions import Coalesce
from django.utils.http import urlsafe_base64_decode
@ -61,8 +62,9 @@ class CaseInsensitiveListFilter(BaseInFilter, CharFilter):
class ProductFilter(FilterSet):
search = CharFilter(field_name="name", method="search_products", label=_("Search"))
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID"))
name = CharFilter(method="filter_name", label=_("Name"))
name = CharFilter(lookup_expr="icontains", label=_("Name"))
categories = CaseInsensitiveListFilter(field_name="category__name", label=_("Categories"))
category_uuid = CharFilter(method="filter_category", label="Category (UUID)")
categories_slugs = CaseInsensitiveListFilter(field_name="category__slug", label=_("Categories Slugs"))
@ -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):

View file

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

View file

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

View file

@ -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(