Merge branch 'main' into storefront-next
This commit is contained in:
commit
63a824d78d
132 changed files with 3497 additions and 1184 deletions
53
.gitlab-ci.yml
Normal file
53
.gitlab-ci.yml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
image: ghcr.io/astral-sh/uv:python3.12-bookworm
|
||||
|
||||
stages:
|
||||
- lint
|
||||
- typecheck
|
||||
- test
|
||||
|
||||
variables:
|
||||
UV_PYTHON: "3.12"
|
||||
PIP_DISABLE_PIP_VERSION_CHECK: "1"
|
||||
PYTHONDONTWRITEBYTECODE: "1"
|
||||
|
||||
before_script:
|
||||
- uv sync --frozen --extra linting
|
||||
|
||||
lint:
|
||||
stage: lint
|
||||
script:
|
||||
- uv run ruff format --check .
|
||||
- uv run ruff check --force-exclude .
|
||||
rules:
|
||||
- changes:
|
||||
- "**/*.py"
|
||||
- "pyproject.toml"
|
||||
- ".pre-commit-config.yaml"
|
||||
- "pyrightconfig.json"
|
||||
when: on_success
|
||||
- when: never
|
||||
|
||||
typecheck:
|
||||
stage: typecheck
|
||||
script:
|
||||
- uv run pyright
|
||||
rules:
|
||||
- changes:
|
||||
- "**/*.py"
|
||||
- "pyproject.toml"
|
||||
- "pyrightconfig.json"
|
||||
when: on_success
|
||||
- when: never
|
||||
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- uv run pytest -q
|
||||
rules:
|
||||
- changes:
|
||||
- "**/*.py"
|
||||
- "pyproject.toml"
|
||||
- "pytest.ini"
|
||||
- "pyproject.toml"
|
||||
when: on_success
|
||||
- when: never
|
||||
18
.pre-commit-config.yaml
Normal file
18
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.9
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: Ruff (lint & fix)
|
||||
args: ["--fix", "--exit-non-zero-on-fix"]
|
||||
files: "\\.(py|pyi)$"
|
||||
exclude: "^storefront/"
|
||||
- id: ruff-format
|
||||
name: Ruff (format)
|
||||
files: "\\.(py|pyi)$"
|
||||
exclude: "^storefront/"
|
||||
|
||||
ci:
|
||||
autofix_commit_msg: "chore(pre-commit): auto-fix issues"
|
||||
autofix_prs: true
|
||||
autoupdate_commit_msg: "chore(pre-commit): autoupdate hooks"
|
||||
|
|
@ -9,7 +9,9 @@ from engine.core.admin import ActivationActionsMixin, FieldsetsMixin
|
|||
|
||||
|
||||
@register(Post)
|
||||
class PostAdmin(SummernoteModelAdminMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class PostAdmin(
|
||||
SummernoteModelAdminMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
|
||||
): # type: ignore [misc, type-arg]
|
||||
list_display = ("title", "author", "slug", "created", "modified")
|
||||
list_filter = ("author", "tags", "created", "modified")
|
||||
search_fields = ("title", "content", "slug")
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ from django.utils.translation import gettext_lazy as _
|
|||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import status
|
||||
|
||||
from engine.core.docs.drf import BASE_ERRORS
|
||||
from engine.blog.serializers import PostSerializer
|
||||
from engine.core.docs.drf import BASE_ERRORS
|
||||
|
||||
POST_SCHEMA = {
|
||||
"list": extend_schema(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ from django_elasticsearch_dsl import fields
|
|||
from django_elasticsearch_dsl.registries import registry
|
||||
|
||||
from engine.blog.models import Post
|
||||
from engine.core.elasticsearch import COMMON_ANALYSIS, ActiveOnlyMixin, add_multilang_fields
|
||||
from engine.core.elasticsearch import (
|
||||
COMMON_ANALYSIS,
|
||||
ActiveOnlyMixin,
|
||||
add_multilang_fields,
|
||||
)
|
||||
from engine.core.elasticsearch.documents import BaseDocument
|
||||
|
||||
|
||||
|
|
@ -12,7 +16,9 @@ class PostDocument(ActiveOnlyMixin, BaseDocument): # type: ignore [misc]
|
|||
analyzer="standard",
|
||||
fields={
|
||||
"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"),
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -48,13 +48,17 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tag_name",
|
||||
models.CharField(
|
||||
help_text="internal tag identifier for the post tag", max_length=255, verbose_name="tag name"
|
||||
help_text="internal tag identifier for the post tag",
|
||||
max_length=255,
|
||||
verbose_name="tag name",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -105,17 +109,26 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
("title", models.CharField()),
|
||||
("content", markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content")),
|
||||
(
|
||||
"content",
|
||||
markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
("file", models.FileField(blank=True, null=True, upload_to="posts/")),
|
||||
("slug", models.SlugField(allow_unicode=True)),
|
||||
(
|
||||
"author",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="posts", to=settings.AUTH_USER_MODEL
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="posts",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
("tags", models.ManyToManyField(to="blog.posttag")),
|
||||
|
|
|
|||
|
|
@ -12,12 +12,21 @@ class Migration(migrations.Migration):
|
|||
model_name="post",
|
||||
name="slug",
|
||||
field=django_extensions.db.fields.AutoSlugField(
|
||||
allow_unicode=True, blank=True, editable=False, populate_from="title", unique=True
|
||||
allow_unicode=True,
|
||||
blank=True,
|
||||
editable=False,
|
||||
populate_from="title",
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="post",
|
||||
name="title",
|
||||
field=models.CharField(help_text="post title", max_length=128, unique=True, verbose_name="title"),
|
||||
field=models.CharField(
|
||||
help_text="post title",
|
||||
max_length=128,
|
||||
unique=True,
|
||||
verbose_name="title",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name="post",
|
||||
name="tags",
|
||||
field=models.ManyToManyField(blank=True, related_name="posts", to="blog.posttag"),
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name="posts", to="blog.posttag"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -11,92 +11,128 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_ar_ar",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_cs_cz",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_da_dk",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_de_de",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_en_gb",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_en_us",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_es_es",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_fr_fr",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_hi_in",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_it_it",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_ja_jp",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_kk_kz",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_nl_nl",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_pl_pl",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_pt_br",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_ro_ro",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_ru_ru",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_zh_hans",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
|
|
|
|||
|
|
@ -11,52 +11,72 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_fa_ir",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_he_il",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_hr_hr",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_id_id",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_ko_kr",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_no_no",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_sv_se",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_th_th",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_tr_tr",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
name="content_vi_vn",
|
||||
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
|
||||
field=markdown_field.fields.MarkdownField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="post",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
from django.conf import settings
|
||||
from django.db.models import CASCADE, CharField, FileField, ForeignKey, ManyToManyField, BooleanField
|
||||
from django.db.models import (
|
||||
CASCADE,
|
||||
BooleanField,
|
||||
CharField,
|
||||
FileField,
|
||||
ForeignKey,
|
||||
ManyToManyField,
|
||||
)
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_extensions.db.fields import AutoSlugField
|
||||
from markdown.extensions.toc import TocExtension
|
||||
|
|
@ -19,9 +26,20 @@ class Post(NiceModel): # type: ignore [django-manager-missing]
|
|||
|
||||
is_publicly_visible = True
|
||||
|
||||
author = ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=CASCADE, blank=False, null=False, related_name="posts")
|
||||
author = ForeignKey(
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
on_delete=CASCADE,
|
||||
blank=False,
|
||||
null=False,
|
||||
related_name="posts",
|
||||
)
|
||||
title = CharField(
|
||||
unique=True, max_length=128, blank=False, null=False, help_text=_("post title"), verbose_name=_("title")
|
||||
unique=True,
|
||||
max_length=128,
|
||||
blank=False,
|
||||
null=False,
|
||||
help_text=_("post title"),
|
||||
verbose_name=_("title"),
|
||||
)
|
||||
content: MarkdownField = MarkdownField(
|
||||
"content",
|
||||
|
|
@ -61,13 +79,17 @@ class Post(NiceModel): # type: ignore [django-manager-missing]
|
|||
null=True,
|
||||
)
|
||||
file = FileField(upload_to="posts/", blank=True, null=True)
|
||||
slug = AutoSlugField(populate_from="title", allow_unicode=True, unique=True, editable=False)
|
||||
slug = AutoSlugField(
|
||||
populate_from="title", allow_unicode=True, unique=True, editable=False
|
||||
)
|
||||
tags = ManyToManyField(to="blog.PostTag", blank=True, related_name="posts")
|
||||
meta_description = CharField(max_length=150, blank=True, null=True)
|
||||
is_static_page = BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("is static page"),
|
||||
help_text=_("is this a post for a page with static URL (e.g. `/help/delivery`)?"),
|
||||
help_text=_(
|
||||
"is this a post for a page with static URL (e.g. `/help/delivery`)?"
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -79,9 +101,15 @@ class Post(NiceModel): # type: ignore [django-manager-missing]
|
|||
|
||||
def save(self, **kwargs):
|
||||
if self.file:
|
||||
raise ValueError(_("markdown files are not supported yet - use markdown content instead"))
|
||||
raise ValueError(
|
||||
_("markdown files are not supported yet - use markdown content instead")
|
||||
)
|
||||
if not any([self.file, self.content]) or all([self.file, self.content]):
|
||||
raise ValueError(_("a markdown file or markdown content must be provided - mutually exclusive"))
|
||||
raise ValueError(
|
||||
_(
|
||||
"a markdown file or markdown content must be provided - mutually exclusive"
|
||||
)
|
||||
)
|
||||
super().save(**kwargs)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ from django_filters.rest_framework import DjangoFilterBackend
|
|||
from drf_spectacular.utils import extend_schema_view
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from engine.blog.docs.drf.viewsets import POST_SCHEMA
|
||||
from engine.blog.filters import PostFilter
|
||||
from engine.blog.models import Post
|
||||
from engine.blog.serializers import PostSerializer
|
||||
from engine.blog.docs.drf.viewsets import POST_SCHEMA
|
||||
from engine.core.permissions import EvibesPermission
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,20 @@ from django.utils.safestring import mark_safe
|
|||
|
||||
class MarkdownEditorWidget(forms.Textarea):
|
||||
class Media:
|
||||
css = {"all": ("https://cdnjs.cloudflare.com/ajax/libs/easymde/2.14.0/easymde.min.css",)}
|
||||
css = {
|
||||
"all": (
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/easymde/2.14.0/easymde.min.css",
|
||||
)
|
||||
}
|
||||
js = ("https://cdnjs.cloudflare.com/ajax/libs/easymde/2.14.0/easymde.min.js",)
|
||||
|
||||
def render(self, name: str, value: str, attrs: dict[Any, Any] | None = None, renderer: BaseRenderer | None = None):
|
||||
def render(
|
||||
self,
|
||||
name: str,
|
||||
value: str,
|
||||
attrs: dict[Any, Any] | None = None,
|
||||
renderer: BaseRenderer | None = None,
|
||||
):
|
||||
if not attrs:
|
||||
attrs = {}
|
||||
attrs["class"] = "markdown-editor"
|
||||
|
|
|
|||
|
|
@ -18,10 +18,16 @@ class NiceModel(Model):
|
|||
is_active = BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("is active"),
|
||||
help_text=_("if set to false, this object can't be seen by users without needed permission"),
|
||||
help_text=_(
|
||||
"if set to false, this object can't be seen by users without needed permission"
|
||||
),
|
||||
)
|
||||
created = CreationDateTimeField(_("created"), help_text=_("when the object first appeared on the database")) # type: ignore [no-untyped-call]
|
||||
modified = ModificationDateTimeField(_("modified"), help_text=_("when the object was last modified")) # type: ignore [no-untyped-call]
|
||||
created = CreationDateTimeField(
|
||||
_("created"), help_text=_("when the object first appeared on the database")
|
||||
) # type: ignore [no-untyped-call]
|
||||
modified = ModificationDateTimeField(
|
||||
_("modified"), help_text=_("when the object was last modified")
|
||||
) # type: ignore [no-untyped-call]
|
||||
|
||||
def save( # type: ignore [override]
|
||||
self,
|
||||
|
|
@ -34,7 +40,10 @@ class NiceModel(Model):
|
|||
) -> None:
|
||||
self.update_modified = update_modified
|
||||
return super().save(
|
||||
force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields
|
||||
force_insert=force_insert,
|
||||
force_update=force_update,
|
||||
using=using,
|
||||
update_fields=update_fields,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
|
|
|||
|
|
@ -33,7 +33,13 @@ from unfold.contrib.import_export.forms import ExportForm, ImportForm
|
|||
from unfold.decorators import action
|
||||
from unfold.widgets import UnfoldAdminSelectWidget, UnfoldAdminTextInputWidget
|
||||
|
||||
from engine.core.forms import CRMForm, OrderForm, OrderProductForm, StockForm, VendorForm
|
||||
from engine.core.forms import (
|
||||
CRMForm,
|
||||
OrderForm,
|
||||
OrderProductForm,
|
||||
StockForm,
|
||||
VendorForm,
|
||||
)
|
||||
from engine.core.models import (
|
||||
Address,
|
||||
Attribute,
|
||||
|
|
@ -64,7 +70,9 @@ class FieldsetsMixin:
|
|||
additional_fields: list[str] | None = []
|
||||
model: ClassVar[Type[Model]]
|
||||
|
||||
def get_fieldsets(self, request: HttpRequest, obj: Any = None) -> list[tuple[str, dict[str, list[str]]]]:
|
||||
def get_fieldsets(
|
||||
self, request: HttpRequest, obj: Any = None
|
||||
) -> list[tuple[str, dict[str, list[str]]]]:
|
||||
if request:
|
||||
pass
|
||||
|
||||
|
|
@ -82,15 +90,29 @@ class FieldsetsMixin:
|
|||
for orig in transoptions.local_fields:
|
||||
translation_fields += get_translation_fields(orig)
|
||||
if translation_fields:
|
||||
fss = list(fss) + [(_("translations"), {"classes": ["tab"], "fields": translation_fields})] # type: ignore [list-item]
|
||||
fss = list(fss) + [
|
||||
(
|
||||
_("translations"),
|
||||
{"classes": ["tab"], "fields": translation_fields},
|
||||
)
|
||||
] # type: ignore [list-item]
|
||||
return fss
|
||||
|
||||
if self.general_fields:
|
||||
fieldsets.append((_("general"), {"classes": ["tab"], "fields": self.general_fields}))
|
||||
fieldsets.append(
|
||||
(_("general"), {"classes": ["tab"], "fields": self.general_fields})
|
||||
)
|
||||
if self.relation_fields:
|
||||
fieldsets.append((_("relations"), {"classes": ["tab"], "fields": self.relation_fields}))
|
||||
fieldsets.append(
|
||||
(_("relations"), {"classes": ["tab"], "fields": self.relation_fields})
|
||||
)
|
||||
if self.additional_fields:
|
||||
fieldsets.append((_("additional info"), {"classes": ["tab"], "fields": self.additional_fields}))
|
||||
fieldsets.append(
|
||||
(
|
||||
_("additional info"),
|
||||
{"classes": ["tab"], "fields": self.additional_fields},
|
||||
)
|
||||
)
|
||||
opts = self.model._meta
|
||||
|
||||
meta_fields = []
|
||||
|
|
@ -108,7 +130,9 @@ class FieldsetsMixin:
|
|||
meta_fields.append("human_readable_id")
|
||||
|
||||
if meta_fields:
|
||||
fieldsets.append((_("metadata"), {"classes": ["tab"], "fields": meta_fields}))
|
||||
fieldsets.append(
|
||||
(_("metadata"), {"classes": ["tab"], "fields": meta_fields})
|
||||
)
|
||||
|
||||
ts = []
|
||||
for name in ("created", "modified"):
|
||||
|
|
@ -130,23 +154,35 @@ class ActivationActionsMixin:
|
|||
"deactivate_selected",
|
||||
]
|
||||
|
||||
@action(description=_("activate selected %(verbose_name_plural)s").lower(), permissions=["change"])
|
||||
@action(
|
||||
description=_("activate selected %(verbose_name_plural)s").lower(),
|
||||
permissions=["change"],
|
||||
)
|
||||
def activate_selected(self, request: HttpRequest, queryset: QuerySet[Any]) -> None:
|
||||
try:
|
||||
queryset.update(is_active=True)
|
||||
self.message_user( # type: ignore [attr-defined]
|
||||
request=request, message=_("selected items have been activated.").lower(), level=messages.SUCCESS
|
||||
request=request,
|
||||
message=_("selected items have been activated.").lower(),
|
||||
level=messages.SUCCESS,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.message_user(request=request, message=str(e), level=messages.ERROR) # type: ignore [attr-defined]
|
||||
|
||||
@action(description=_("deactivate selected %(verbose_name_plural)s").lower(), permissions=["change"])
|
||||
def deactivate_selected(self, request: HttpRequest, queryset: QuerySet[Any]) -> None:
|
||||
@action(
|
||||
description=_("deactivate selected %(verbose_name_plural)s").lower(),
|
||||
permissions=["change"],
|
||||
)
|
||||
def deactivate_selected(
|
||||
self, request: HttpRequest, queryset: QuerySet[Any]
|
||||
) -> None:
|
||||
try:
|
||||
queryset.update(is_active=False)
|
||||
self.message_user( # type: ignore [attr-defined]
|
||||
request=request, message=_("selected items have been deactivated.").lower(), level=messages.SUCCESS
|
||||
request=request,
|
||||
message=_("selected items have been deactivated.").lower(),
|
||||
level=messages.SUCCESS,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -198,7 +234,12 @@ class OrderProductInline(TabularInline): # type: ignore [type-arg]
|
|||
tab = True
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related("product").only("product__name")
|
||||
return (
|
||||
super()
|
||||
.get_queryset(request)
|
||||
.select_related("product")
|
||||
.only("product__name")
|
||||
)
|
||||
|
||||
|
||||
class CategoryChildrenInline(TabularInline): # type: ignore [type-arg]
|
||||
|
|
@ -212,7 +253,9 @@ class CategoryChildrenInline(TabularInline): # type: ignore [type-arg]
|
|||
|
||||
|
||||
@register(AttributeGroup)
|
||||
class AttributeGroupAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class AttributeGroupAdmin(
|
||||
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
|
||||
): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = AttributeGroup # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -236,7 +279,9 @@ class AttributeGroupAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActions
|
|||
|
||||
|
||||
@register(Attribute)
|
||||
class AttributeAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class AttributeAdmin(
|
||||
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
|
||||
): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Attribute # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -311,7 +356,13 @@ class AttributeValueAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin):
|
|||
|
||||
|
||||
@register(Category)
|
||||
class CategoryAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, DraggableMPTTAdmin, ModelAdmin):
|
||||
class CategoryAdmin(
|
||||
DjangoQLSearchMixin,
|
||||
FieldsetsMixin,
|
||||
ActivationActionsMixin,
|
||||
DraggableMPTTAdmin,
|
||||
ModelAdmin,
|
||||
):
|
||||
# noinspection PyClassVar
|
||||
model = Category
|
||||
list_display = (
|
||||
|
|
@ -360,7 +411,9 @@ class CategoryAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin,
|
|||
|
||||
|
||||
@register(Brand)
|
||||
class BrandAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class BrandAdmin(
|
||||
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
|
||||
): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Brand # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -392,7 +445,13 @@ class BrandAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, Mo
|
|||
|
||||
|
||||
@register(Product)
|
||||
class ProductAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin, ImportExportModelAdmin): # type: ignore [misc, type-arg]
|
||||
class ProductAdmin(
|
||||
DjangoQLSearchMixin,
|
||||
FieldsetsMixin,
|
||||
ActivationActionsMixin,
|
||||
ModelAdmin,
|
||||
ImportExportModelAdmin,
|
||||
): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Product # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -471,7 +530,9 @@ class ProductAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin,
|
|||
|
||||
|
||||
@register(ProductTag)
|
||||
class ProductTagAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class ProductTagAdmin(
|
||||
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
|
||||
): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = ProductTag # type: ignore [misc]
|
||||
list_display = ("tag_name",)
|
||||
|
|
@ -489,7 +550,9 @@ class ProductTagAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixi
|
|||
|
||||
|
||||
@register(CategoryTag)
|
||||
class CategoryTagAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class CategoryTagAdmin(
|
||||
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
|
||||
): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = CategoryTag # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -515,7 +578,9 @@ class CategoryTagAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMix
|
|||
|
||||
|
||||
@register(Vendor)
|
||||
class VendorAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class VendorAdmin(
|
||||
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
|
||||
): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Vendor # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -555,7 +620,9 @@ class VendorAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, M
|
|||
|
||||
|
||||
@register(Feedback)
|
||||
class FeedbackAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class FeedbackAdmin(
|
||||
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
|
||||
): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Feedback # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -588,7 +655,9 @@ class FeedbackAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin,
|
|||
|
||||
|
||||
@register(Order)
|
||||
class OrderAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class OrderAdmin(
|
||||
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
|
||||
): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Order # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -639,7 +708,9 @@ class OrderAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, Mo
|
|||
|
||||
|
||||
@register(OrderProduct)
|
||||
class OrderProductAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class OrderProductAdmin(
|
||||
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
|
||||
): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = OrderProduct # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -677,7 +748,9 @@ class OrderProductAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMi
|
|||
|
||||
|
||||
@register(PromoCode)
|
||||
class PromoCodeAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class PromoCodeAdmin(
|
||||
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
|
||||
): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = PromoCode # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -721,7 +794,9 @@ class PromoCodeAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin
|
|||
|
||||
|
||||
@register(Promotion)
|
||||
class PromotionAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class PromotionAdmin(
|
||||
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
|
||||
): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Promotion # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -748,7 +823,9 @@ class PromotionAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin
|
|||
|
||||
|
||||
@register(Stock)
|
||||
class StockAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class StockAdmin(
|
||||
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
|
||||
): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Stock # type: ignore [misc]
|
||||
form = StockForm
|
||||
|
|
@ -796,7 +873,9 @@ class StockAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, Mo
|
|||
|
||||
|
||||
@register(Wishlist)
|
||||
class WishlistAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class WishlistAdmin(
|
||||
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
|
||||
): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Wishlist # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -822,7 +901,9 @@ class WishlistAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin,
|
|||
|
||||
|
||||
@register(ProductImage)
|
||||
class ProductImageAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class ProductImageAdmin(
|
||||
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
|
||||
): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = ProductImage # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -908,7 +989,9 @@ class AddressAdmin(DjangoQLSearchMixin, FieldsetsMixin, GISModelAdmin): # type:
|
|||
|
||||
|
||||
@register(CustomerRelationshipManagementProvider)
|
||||
class CustomerRelationshipManagementProviderAdmin(DjangoQLSearchMixin, FieldsetsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class CustomerRelationshipManagementProviderAdmin(
|
||||
DjangoQLSearchMixin, FieldsetsMixin, ModelAdmin
|
||||
): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = CustomerRelationshipManagementProvider # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
|
|||
|
|
@ -9,5 +9,9 @@ app_name = "core"
|
|||
|
||||
urlpatterns = [
|
||||
path("search/", GlobalSearchView.as_view(), name="global_search"),
|
||||
path("orders/buy_as_business/", BuyAsBusinessView.as_view(), name="request_cursed_url"),
|
||||
path(
|
||||
"orders/buy_as_business/",
|
||||
BuyAsBusinessView.as_view(),
|
||||
name="request_cursed_url",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ from django.core.cache import cache
|
|||
from django.db import transaction
|
||||
|
||||
from engine.core.crm.exceptions import CRMException
|
||||
from engine.core.models import CustomerRelationshipManagementProvider, Order, OrderCrmLink
|
||||
from engine.core.models import (
|
||||
CustomerRelationshipManagementProvider,
|
||||
Order,
|
||||
OrderCrmLink,
|
||||
)
|
||||
from engine.core.utils import is_status_code_success
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -17,7 +21,9 @@ class AmoCRM:
|
|||
|
||||
def __init__(self):
|
||||
try:
|
||||
self.instance = CustomerRelationshipManagementProvider.objects.get(name="AmoCRM")
|
||||
self.instance = CustomerRelationshipManagementProvider.objects.get(
|
||||
name="AmoCRM"
|
||||
)
|
||||
except CustomerRelationshipManagementProvider.DoesNotExist as dne:
|
||||
logger.warning("AMO CRM provider not found")
|
||||
raise CRMException("AMO CRM provider not found") from dne
|
||||
|
|
@ -70,7 +76,10 @@ class AmoCRM:
|
|||
return self.access_token
|
||||
|
||||
def _headers(self) -> dict:
|
||||
return {"Authorization": f"Bearer {self._token()}", "Content-Type": "application/json"}
|
||||
return {
|
||||
"Authorization": f"Bearer {self._token()}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
def _build_lead_payload(self, order: Order) -> dict:
|
||||
name = f"Заказ #{order.human_readable_id}"
|
||||
|
|
@ -104,7 +113,8 @@ class AmoCRM:
|
|||
)
|
||||
try:
|
||||
r = requests.get(
|
||||
f"https://api-fns.ru/api/egr?req={order.business_identificator}&key={self.fns_api_key}", timeout=15
|
||||
f"https://api-fns.ru/api/egr?req={order.business_identificator}&key={self.fns_api_key}",
|
||||
timeout=15,
|
||||
)
|
||||
r.raise_for_status()
|
||||
body = r.json()
|
||||
|
|
@ -129,28 +139,43 @@ class AmoCRM:
|
|||
if customer_name:
|
||||
r = requests.get(
|
||||
f"{self.base}/api/v4/contacts",
|
||||
headers=self._headers().update({"filter[name]": customer_name, "limit": 1}),
|
||||
headers=self._headers().update(
|
||||
{"filter[name]": customer_name, "limit": 1}
|
||||
),
|
||||
timeout=15,
|
||||
)
|
||||
if r.status_code == 200:
|
||||
body = r.json()
|
||||
return body.get("_embedded", {}).get("contacts", [{}])[0].get("id", None)
|
||||
return (
|
||||
body.get("_embedded", {})
|
||||
.get("contacts", [{}])[0]
|
||||
.get("id", None)
|
||||
)
|
||||
create_contact_payload = {"name": customer_name}
|
||||
|
||||
if self.responsible_user_id:
|
||||
create_contact_payload["responsible_user_id"] = self.responsible_user_id
|
||||
create_contact_payload["responsible_user_id"] = (
|
||||
self.responsible_user_id
|
||||
)
|
||||
|
||||
if order.user:
|
||||
create_contact_payload["first_name"] = order.user.first_name or ""
|
||||
create_contact_payload["last_name"] = order.user.last_name or ""
|
||||
|
||||
r = requests.post(
|
||||
f"{self.base}/api/v4/contacts", json={"name": customer_name}, headers=self._headers(), timeout=15
|
||||
f"{self.base}/api/v4/contacts",
|
||||
json={"name": customer_name},
|
||||
headers=self._headers(),
|
||||
timeout=15,
|
||||
)
|
||||
|
||||
if r.status_code == 200:
|
||||
body = r.json()
|
||||
return body.get("_embedded", {}).get("contacts", [{}])[0].get("id", None)
|
||||
return (
|
||||
body.get("_embedded", {})
|
||||
.get("contacts", [{}])[0]
|
||||
.get("id", None)
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -173,17 +198,27 @@ class AmoCRM:
|
|||
lead_id = link.crm_lead_id
|
||||
payload = self._build_lead_payload(order)
|
||||
r = requests.patch(
|
||||
f"{self.base}/api/v4/leads/{lead_id}", json=payload, headers=self._headers(), timeout=15
|
||||
f"{self.base}/api/v4/leads/{lead_id}",
|
||||
json=payload,
|
||||
headers=self._headers(),
|
||||
timeout=15,
|
||||
)
|
||||
if r.status_code not in (200, 204):
|
||||
r.raise_for_status()
|
||||
return lead_id
|
||||
payload = self._build_lead_payload(order)
|
||||
r = requests.post(f"{self.base}/api/v4/leads", json=[payload], headers=self._headers(), timeout=15)
|
||||
r = requests.post(
|
||||
f"{self.base}/api/v4/leads",
|
||||
json=[payload],
|
||||
headers=self._headers(),
|
||||
timeout=15,
|
||||
)
|
||||
r.raise_for_status()
|
||||
body = r.json()
|
||||
lead_id = str(body["_embedded"]["leads"][0]["id"])
|
||||
OrderCrmLink.objects.create(order=order, crm_lead_id=lead_id, crm=self.instance)
|
||||
OrderCrmLink.objects.create(
|
||||
order=order, crm_lead_id=lead_id, crm=self.instance
|
||||
)
|
||||
return lead_id
|
||||
|
||||
def update_order_status(self, crm_lead_id: str, new_status: str) -> None:
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ from engine.core.serializers import (
|
|||
)
|
||||
from engine.payments.serializers import TransactionProcessSerializer
|
||||
|
||||
|
||||
CUSTOM_OPENAPI_SCHEMA = {
|
||||
"get": extend_schema(
|
||||
tags=[
|
||||
|
|
@ -48,7 +47,9 @@ CACHE_SCHEMA = {
|
|||
),
|
||||
request=CacheOperatorSerializer,
|
||||
responses={
|
||||
status.HTTP_200_OK: inline_serializer("cache", fields={"data": JSONField()}),
|
||||
status.HTTP_200_OK: inline_serializer(
|
||||
"cache", fields={"data": JSONField()}
|
||||
),
|
||||
status.HTTP_400_BAD_REQUEST: error,
|
||||
},
|
||||
),
|
||||
|
|
@ -72,7 +73,11 @@ PARAMETERS_SCHEMA = {
|
|||
"misc",
|
||||
],
|
||||
summary=_("get application's exposable parameters"),
|
||||
responses={status.HTTP_200_OK: inline_serializer("parameters", fields={"key": CharField(default="value")})},
|
||||
responses={
|
||||
status.HTTP_200_OK: inline_serializer(
|
||||
"parameters", fields={"key": CharField(default="value")}
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +101,9 @@ REQUEST_CURSED_URL_SCHEMA = {
|
|||
"misc",
|
||||
],
|
||||
summary=_("request a CORSed URL"),
|
||||
request=inline_serializer("url", fields={"url": CharField(default="https://example.org")}),
|
||||
request=inline_serializer(
|
||||
"url", fields={"url": CharField(default="https://example.org")}
|
||||
),
|
||||
responses={
|
||||
status.HTTP_200_OK: inline_serializer("data", fields={"data": JSONField()}),
|
||||
status.HTTP_400_BAD_REQUEST: error,
|
||||
|
|
@ -121,7 +128,11 @@ SEARCH_SCHEMA = {
|
|||
responses={
|
||||
status.HTTP_200_OK: inline_serializer(
|
||||
name="GlobalSearchResponse",
|
||||
fields={"results": DictField(child=ListField(child=DictField(child=CharField())))},
|
||||
fields={
|
||||
"results": DictField(
|
||||
child=ListField(child=DictField(child=CharField()))
|
||||
)
|
||||
},
|
||||
),
|
||||
status.HTTP_400_BAD_REQUEST: inline_serializer(
|
||||
name="GlobalSearchErrorResponse", fields={"error": CharField()}
|
||||
|
|
@ -143,7 +154,9 @@ BUY_AS_BUSINESS_SCHEMA = {
|
|||
status.HTTP_400_BAD_REQUEST: error,
|
||||
},
|
||||
description=(
|
||||
_("purchase an order as a business, using the provided `products` with `product_uuid` and `attributes`.")
|
||||
_(
|
||||
"purchase an order as a business, using the provided `products` with `product_uuid` and `attributes`."
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,11 @@ from engine.core.serializers import (
|
|||
WishlistSimpleSerializer,
|
||||
)
|
||||
from engine.core.serializers.seo import SeoSnapshotSerializer
|
||||
from engine.core.serializers.utility import AddressCreateSerializer, AddressSuggestionSerializer, DoFeedbackSerializer
|
||||
from engine.core.serializers.utility import (
|
||||
AddressCreateSerializer,
|
||||
AddressSuggestionSerializer,
|
||||
DoFeedbackSerializer,
|
||||
)
|
||||
from engine.payments.serializers import TransactionProcessSerializer
|
||||
|
||||
ATTRIBUTE_GROUP_SCHEMA = {
|
||||
|
|
@ -59,7 +63,10 @@ ATTRIBUTE_GROUP_SCHEMA = {
|
|||
"attributeGroups",
|
||||
],
|
||||
summary=_("list all attribute groups (simple view)"),
|
||||
responses={status.HTTP_200_OK: AttributeGroupSimpleSerializer(many=True), **BASE_ERRORS},
|
||||
responses={
|
||||
status.HTTP_200_OK: AttributeGroupSimpleSerializer(many=True),
|
||||
**BASE_ERRORS,
|
||||
},
|
||||
),
|
||||
"retrieve": extend_schema(
|
||||
tags=[
|
||||
|
|
@ -73,7 +80,10 @@ ATTRIBUTE_GROUP_SCHEMA = {
|
|||
"attributeGroups",
|
||||
],
|
||||
summary=_("create an attribute group"),
|
||||
responses={status.HTTP_201_CREATED: AttributeGroupDetailSerializer(), **BASE_ERRORS},
|
||||
responses={
|
||||
status.HTTP_201_CREATED: AttributeGroupDetailSerializer(),
|
||||
**BASE_ERRORS,
|
||||
},
|
||||
),
|
||||
"destroy": extend_schema(
|
||||
tags=[
|
||||
|
|
@ -93,7 +103,9 @@ ATTRIBUTE_GROUP_SCHEMA = {
|
|||
tags=[
|
||||
"attributeGroups",
|
||||
],
|
||||
summary=_("rewrite some fields of an existing attribute group saving non-editables"),
|
||||
summary=_(
|
||||
"rewrite some fields of an existing attribute group saving non-editables"
|
||||
),
|
||||
responses={status.HTTP_200_OK: AttributeGroupDetailSerializer(), **BASE_ERRORS},
|
||||
),
|
||||
}
|
||||
|
|
@ -104,7 +116,10 @@ ATTRIBUTE_SCHEMA = {
|
|||
"attributes",
|
||||
],
|
||||
summary=_("list all attributes (simple view)"),
|
||||
responses={status.HTTP_200_OK: AttributeSimpleSerializer(many=True), **BASE_ERRORS},
|
||||
responses={
|
||||
status.HTTP_200_OK: AttributeSimpleSerializer(many=True),
|
||||
**BASE_ERRORS,
|
||||
},
|
||||
),
|
||||
"retrieve": extend_schema(
|
||||
tags=[
|
||||
|
|
@ -149,7 +164,10 @@ ATTRIBUTE_VALUE_SCHEMA = {
|
|||
"attributeValues",
|
||||
],
|
||||
summary=_("list all attribute values (simple view)"),
|
||||
responses={status.HTTP_200_OK: AttributeValueSimpleSerializer(many=True), **BASE_ERRORS},
|
||||
responses={
|
||||
status.HTTP_200_OK: AttributeValueSimpleSerializer(many=True),
|
||||
**BASE_ERRORS,
|
||||
},
|
||||
),
|
||||
"retrieve": extend_schema(
|
||||
tags=[
|
||||
|
|
@ -163,7 +181,10 @@ ATTRIBUTE_VALUE_SCHEMA = {
|
|||
"attributeValues",
|
||||
],
|
||||
summary=_("create an attribute value"),
|
||||
responses={status.HTTP_201_CREATED: AttributeValueDetailSerializer(), **BASE_ERRORS},
|
||||
responses={
|
||||
status.HTTP_201_CREATED: AttributeValueDetailSerializer(),
|
||||
**BASE_ERRORS,
|
||||
},
|
||||
),
|
||||
"destroy": extend_schema(
|
||||
tags=[
|
||||
|
|
@ -183,7 +204,9 @@ ATTRIBUTE_VALUE_SCHEMA = {
|
|||
tags=[
|
||||
"attributeValues",
|
||||
],
|
||||
summary=_("rewrite some fields of an existing attribute value saving non-editables"),
|
||||
summary=_(
|
||||
"rewrite some fields of an existing attribute value saving non-editables"
|
||||
),
|
||||
responses={status.HTTP_200_OK: AttributeValueDetailSerializer(), **BASE_ERRORS},
|
||||
),
|
||||
}
|
||||
|
|
@ -195,7 +218,10 @@ CATEGORY_SCHEMA = {
|
|||
],
|
||||
summary=_("list all categories (simple view)"),
|
||||
description=_("list all categories (simple view)"),
|
||||
responses={status.HTTP_200_OK: CategorySimpleSerializer(many=True), **BASE_ERRORS},
|
||||
responses={
|
||||
status.HTTP_200_OK: CategorySimpleSerializer(many=True),
|
||||
**BASE_ERRORS,
|
||||
},
|
||||
),
|
||||
"retrieve": extend_schema(
|
||||
tags=[
|
||||
|
|
@ -242,7 +268,9 @@ CATEGORY_SCHEMA = {
|
|||
"categories",
|
||||
],
|
||||
summary=_("rewrite some fields of an existing category saving non-editables"),
|
||||
description=_("rewrite some fields of an existing category saving non-editables"),
|
||||
description=_(
|
||||
"rewrite some fields of an existing category saving non-editables"
|
||||
),
|
||||
responses={status.HTTP_200_OK: CategoryDetailSerializer(), **BASE_ERRORS},
|
||||
),
|
||||
"seo_meta": extend_schema(
|
||||
|
|
@ -315,7 +343,9 @@ ORDER_SCHEMA = {
|
|||
OpenApiParameter(
|
||||
name="status",
|
||||
type=OpenApiTypes.STR,
|
||||
description=_("Filter by order status (case-insensitive substring match)"),
|
||||
description=_(
|
||||
"Filter by order status (case-insensitive substring match)"
|
||||
),
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="order_by",
|
||||
|
|
@ -418,7 +448,9 @@ ORDER_SCHEMA = {
|
|||
"orders",
|
||||
],
|
||||
summary=_("add product to order"),
|
||||
description=_("adds a product to an order using the provided `product_uuid` and `attributes`."),
|
||||
description=_(
|
||||
"adds a product to an order using the provided `product_uuid` and `attributes`."
|
||||
),
|
||||
request=AddOrderProductSerializer(),
|
||||
responses={status.HTTP_200_OK: OrderDetailSerializer(), **BASE_ERRORS},
|
||||
),
|
||||
|
|
@ -427,7 +459,9 @@ ORDER_SCHEMA = {
|
|||
"orders",
|
||||
],
|
||||
summary=_("add a list of products to order, quantities will not count"),
|
||||
description=_("adds a list of products to an order using the provided `product_uuid` and `attributes`."),
|
||||
description=_(
|
||||
"adds a list of products to an order using the provided `product_uuid` and `attributes`."
|
||||
),
|
||||
request=BulkAddOrderProductsSerializer(),
|
||||
responses={status.HTTP_200_OK: OrderDetailSerializer(), **BASE_ERRORS},
|
||||
),
|
||||
|
|
@ -436,7 +470,9 @@ ORDER_SCHEMA = {
|
|||
"orders",
|
||||
],
|
||||
summary=_("remove product from order"),
|
||||
description=_("removes a product from an order using the provided `product_uuid` and `attributes`."),
|
||||
description=_(
|
||||
"removes a product from an order using the provided `product_uuid` and `attributes`."
|
||||
),
|
||||
request=RemoveOrderProductSerializer(),
|
||||
responses={status.HTTP_200_OK: OrderDetailSerializer(), **BASE_ERRORS},
|
||||
),
|
||||
|
|
@ -445,7 +481,9 @@ ORDER_SCHEMA = {
|
|||
"orders",
|
||||
],
|
||||
summary=_("remove product from order, quantities will not count"),
|
||||
description=_("removes a list of products from an order using the provided `product_uuid` and `attributes`"),
|
||||
description=_(
|
||||
"removes a list of products from an order using the provided `product_uuid` and `attributes`"
|
||||
),
|
||||
request=BulkRemoveOrderProductsSerializer(),
|
||||
responses={status.HTTP_200_OK: OrderDetailSerializer(), **BASE_ERRORS},
|
||||
),
|
||||
|
|
@ -458,7 +496,10 @@ WISHLIST_SCHEMA = {
|
|||
],
|
||||
summary=_("list all wishlists (simple view)"),
|
||||
description=_("for non-staff users, only their own wishlists are returned."),
|
||||
responses={status.HTTP_200_OK: WishlistSimpleSerializer(many=True), **BASE_ERRORS},
|
||||
responses={
|
||||
status.HTTP_200_OK: WishlistSimpleSerializer(many=True),
|
||||
**BASE_ERRORS,
|
||||
},
|
||||
),
|
||||
"retrieve": extend_schema(
|
||||
tags=[
|
||||
|
|
@ -512,7 +553,9 @@ WISHLIST_SCHEMA = {
|
|||
"wishlists",
|
||||
],
|
||||
summary=_("add product to wishlist"),
|
||||
description=_("adds a product to an wishlist using the provided `product_uuid`"),
|
||||
description=_(
|
||||
"adds a product to an wishlist using the provided `product_uuid`"
|
||||
),
|
||||
request=AddWishlistProductSerializer(),
|
||||
responses={status.HTTP_200_OK: WishlistDetailSerializer(), **BASE_ERRORS},
|
||||
),
|
||||
|
|
@ -521,7 +564,9 @@ WISHLIST_SCHEMA = {
|
|||
"wishlists",
|
||||
],
|
||||
summary=_("remove product from wishlist"),
|
||||
description=_("removes a product from an wishlist using the provided `product_uuid`"),
|
||||
description=_(
|
||||
"removes a product from an wishlist using the provided `product_uuid`"
|
||||
),
|
||||
request=RemoveWishlistProductSerializer(),
|
||||
responses={status.HTTP_200_OK: WishlistDetailSerializer(), **BASE_ERRORS},
|
||||
),
|
||||
|
|
@ -530,7 +575,9 @@ WISHLIST_SCHEMA = {
|
|||
"wishlists",
|
||||
],
|
||||
summary=_("add many products to wishlist"),
|
||||
description=_("adds many products to an wishlist using the provided `product_uuids`"),
|
||||
description=_(
|
||||
"adds many products to an wishlist using the provided `product_uuids`"
|
||||
),
|
||||
request=BulkAddWishlistProductSerializer(),
|
||||
responses={status.HTTP_200_OK: WishlistDetailSerializer(), **BASE_ERRORS},
|
||||
),
|
||||
|
|
@ -539,7 +586,9 @@ WISHLIST_SCHEMA = {
|
|||
"wishlists",
|
||||
],
|
||||
summary=_("remove many products from wishlist"),
|
||||
description=_("removes many products from an wishlist using the provided `product_uuids`"),
|
||||
description=_(
|
||||
"removes many products from an wishlist using the provided `product_uuids`"
|
||||
),
|
||||
request=BulkRemoveWishlistProductSerializer(),
|
||||
responses={status.HTTP_200_OK: WishlistDetailSerializer(), **BASE_ERRORS},
|
||||
),
|
||||
|
|
@ -644,8 +693,12 @@ PRODUCT_SCHEMA = {
|
|||
tags=[
|
||||
"products",
|
||||
],
|
||||
summary=_("update some fields of an existing product, preserving non-editable fields"),
|
||||
description=_("update some fields of an existing product, preserving non-editable fields"),
|
||||
summary=_(
|
||||
"update some fields of an existing product, preserving non-editable fields"
|
||||
),
|
||||
description=_(
|
||||
"update some fields of an existing product, preserving non-editable fields"
|
||||
),
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="lookup_value",
|
||||
|
|
@ -791,7 +844,9 @@ ADDRESS_SCHEMA = {
|
|||
OpenApiParameter(
|
||||
name="q",
|
||||
location="query",
|
||||
description=_("raw data query string, please append with data from geo-IP endpoint"),
|
||||
description=_(
|
||||
"raw data query string, please append with data from geo-IP endpoint"
|
||||
),
|
||||
type=str,
|
||||
),
|
||||
OpenApiParameter(
|
||||
|
|
@ -814,7 +869,10 @@ FEEDBACK_SCHEMA = {
|
|||
"feedbacks",
|
||||
],
|
||||
summary=_("list all feedbacks (simple view)"),
|
||||
responses={status.HTTP_200_OK: FeedbackSimpleSerializer(many=True), **BASE_ERRORS},
|
||||
responses={
|
||||
status.HTTP_200_OK: FeedbackSimpleSerializer(many=True),
|
||||
**BASE_ERRORS,
|
||||
},
|
||||
),
|
||||
"retrieve": extend_schema(
|
||||
tags=[
|
||||
|
|
@ -1004,7 +1062,10 @@ VENDOR_SCHEMA = {
|
|||
"vendors",
|
||||
],
|
||||
summary=_("list all vendors (simple view)"),
|
||||
responses={status.HTTP_200_OK: VendorSimpleSerializer(many=True), **BASE_ERRORS},
|
||||
responses={
|
||||
status.HTTP_200_OK: VendorSimpleSerializer(many=True),
|
||||
**BASE_ERRORS,
|
||||
},
|
||||
),
|
||||
"retrieve": extend_schema(
|
||||
tags=[
|
||||
|
|
@ -1049,7 +1110,10 @@ PRODUCT_IMAGE_SCHEMA = {
|
|||
"productImages",
|
||||
],
|
||||
summary=_("list all product images (simple view)"),
|
||||
responses={status.HTTP_200_OK: ProductImageSimpleSerializer(many=True), **BASE_ERRORS},
|
||||
responses={
|
||||
status.HTTP_200_OK: ProductImageSimpleSerializer(many=True),
|
||||
**BASE_ERRORS,
|
||||
},
|
||||
),
|
||||
"retrieve": extend_schema(
|
||||
tags=[
|
||||
|
|
@ -1063,7 +1127,10 @@ PRODUCT_IMAGE_SCHEMA = {
|
|||
"productImages",
|
||||
],
|
||||
summary=_("create a product image"),
|
||||
responses={status.HTTP_201_CREATED: ProductImageDetailSerializer(), **BASE_ERRORS},
|
||||
responses={
|
||||
status.HTTP_201_CREATED: ProductImageDetailSerializer(),
|
||||
**BASE_ERRORS,
|
||||
},
|
||||
),
|
||||
"destroy": extend_schema(
|
||||
tags=[
|
||||
|
|
@ -1083,7 +1150,9 @@ PRODUCT_IMAGE_SCHEMA = {
|
|||
tags=[
|
||||
"productImages",
|
||||
],
|
||||
summary=_("rewrite some fields of an existing product image saving non-editables"),
|
||||
summary=_(
|
||||
"rewrite some fields of an existing product image saving non-editables"
|
||||
),
|
||||
responses={status.HTTP_200_OK: ProductImageDetailSerializer(), **BASE_ERRORS},
|
||||
),
|
||||
}
|
||||
|
|
@ -1094,7 +1163,10 @@ PROMOCODE_SCHEMA = {
|
|||
"promocodes",
|
||||
],
|
||||
summary=_("list all promo codes (simple view)"),
|
||||
responses={status.HTTP_200_OK: PromoCodeSimpleSerializer(many=True), **BASE_ERRORS},
|
||||
responses={
|
||||
status.HTTP_200_OK: PromoCodeSimpleSerializer(many=True),
|
||||
**BASE_ERRORS,
|
||||
},
|
||||
),
|
||||
"retrieve": extend_schema(
|
||||
tags=[
|
||||
|
|
@ -1139,7 +1211,10 @@ PROMOTION_SCHEMA = {
|
|||
"promotions",
|
||||
],
|
||||
summary=_("list all promotions (simple view)"),
|
||||
responses={status.HTTP_200_OK: PromotionSimpleSerializer(many=True), **BASE_ERRORS},
|
||||
responses={
|
||||
status.HTTP_200_OK: PromotionSimpleSerializer(many=True),
|
||||
**BASE_ERRORS,
|
||||
},
|
||||
),
|
||||
"retrieve": extend_schema(
|
||||
tags=[
|
||||
|
|
@ -1218,7 +1293,9 @@ STOCK_SCHEMA = {
|
|||
tags=[
|
||||
"stocks",
|
||||
],
|
||||
summary=_("rewrite some fields of an existing stock record saving non-editables"),
|
||||
summary=_(
|
||||
"rewrite some fields of an existing stock record saving non-editables"
|
||||
),
|
||||
responses={status.HTTP_200_OK: StockDetailSerializer(), **BASE_ERRORS},
|
||||
),
|
||||
}
|
||||
|
|
@ -1229,7 +1306,10 @@ PRODUCT_TAG_SCHEMA = {
|
|||
"productTags",
|
||||
],
|
||||
summary=_("list all product tags (simple view)"),
|
||||
responses={status.HTTP_200_OK: ProductTagSimpleSerializer(many=True), **BASE_ERRORS},
|
||||
responses={
|
||||
status.HTTP_200_OK: ProductTagSimpleSerializer(many=True),
|
||||
**BASE_ERRORS,
|
||||
},
|
||||
),
|
||||
"retrieve": extend_schema(
|
||||
tags=[
|
||||
|
|
@ -1243,7 +1323,10 @@ PRODUCT_TAG_SCHEMA = {
|
|||
"productTags",
|
||||
],
|
||||
summary=_("create a product tag"),
|
||||
responses={status.HTTP_201_CREATED: ProductTagDetailSerializer(), **BASE_ERRORS},
|
||||
responses={
|
||||
status.HTTP_201_CREATED: ProductTagDetailSerializer(),
|
||||
**BASE_ERRORS,
|
||||
},
|
||||
),
|
||||
"destroy": extend_schema(
|
||||
tags=[
|
||||
|
|
@ -1263,7 +1346,9 @@ PRODUCT_TAG_SCHEMA = {
|
|||
tags=[
|
||||
"productTags",
|
||||
],
|
||||
summary=_("rewrite some fields of an existing product tag saving non-editables"),
|
||||
summary=_(
|
||||
"rewrite some fields of an existing product tag saving non-editables"
|
||||
),
|
||||
responses={status.HTTP_200_OK: ProductTagDetailSerializer(), **BASE_ERRORS},
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import re
|
||||
from typing import Any
|
||||
from typing import Any, Callable
|
||||
|
||||
from typing import Callable
|
||||
from django.conf import settings
|
||||
from django.db.models import QuerySet
|
||||
from django.http import Http404
|
||||
|
|
@ -86,7 +85,13 @@ functions = [
|
|||
"weight": 0.3,
|
||||
},
|
||||
{
|
||||
"filter": Q("bool", must=[Q("term", **{"_index": "products"}), Q("term", **{"personal_orders_only": False})]),
|
||||
"filter": Q(
|
||||
"bool",
|
||||
must=[
|
||||
Q("term", **{"_index": "products"}),
|
||||
Q("term", **{"personal_orders_only": False}),
|
||||
],
|
||||
),
|
||||
"weight": 0.7,
|
||||
},
|
||||
{
|
||||
|
|
@ -176,9 +181,15 @@ def process_query(
|
|||
if is_code_like:
|
||||
text_shoulds.extend(
|
||||
[
|
||||
Q("term", **{"partnumber.raw": {"value": query.lower(), "boost": 14.0}}),
|
||||
Q(
|
||||
"term",
|
||||
**{"partnumber.raw": {"value": query.lower(), "boost": 14.0}},
|
||||
),
|
||||
Q("term", **{"sku.raw": {"value": query.lower(), "boost": 12.0}}),
|
||||
Q("prefix", **{"partnumber.raw": {"value": query.lower(), "boost": 4.0}}),
|
||||
Q(
|
||||
"prefix",
|
||||
**{"partnumber.raw": {"value": query.lower(), "boost": 4.0}},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
|
@ -227,14 +238,25 @@ def process_query(
|
|||
search_products = build_search(["products"], size=33)
|
||||
resp_products = search_products.execute()
|
||||
|
||||
results: dict[str, list[dict[str, Any]]] = {"products": [], "categories": [], "brands": [], "posts": []}
|
||||
uuids_by_index: dict[str, list[str]] = {"products": [], "categories": [], "brands": []}
|
||||
results: dict[str, list[dict[str, Any]]] = {
|
||||
"products": [],
|
||||
"categories": [],
|
||||
"brands": [],
|
||||
"posts": [],
|
||||
}
|
||||
uuids_by_index: dict[str, list[str]] = {
|
||||
"products": [],
|
||||
"categories": [],
|
||||
"brands": [],
|
||||
}
|
||||
hit_cache: list[Any] = []
|
||||
|
||||
seen_keys: set[tuple[str, str]] = set()
|
||||
|
||||
def _hit_key(hittee: Any) -> tuple[str, str]:
|
||||
return hittee.meta.index, str(getattr(hittee, "uuid", None) or hittee.meta.id)
|
||||
return hittee.meta.index, str(
|
||||
getattr(hittee, "uuid", None) or hittee.meta.id
|
||||
)
|
||||
|
||||
def _collect_hits(hits: list[Any]) -> None:
|
||||
for hh in hits:
|
||||
|
|
@ -267,7 +289,12 @@ def process_query(
|
|||
]
|
||||
for qx in product_exact_sequence:
|
||||
try:
|
||||
resp_exact = Search(index=["products"]).query(qx).extra(size=5, track_total_hits=False).execute()
|
||||
resp_exact = (
|
||||
Search(index=["products"])
|
||||
.query(qx)
|
||||
.extra(size=5, track_total_hits=False)
|
||||
.execute()
|
||||
)
|
||||
except NotFoundError:
|
||||
resp_exact = None
|
||||
if resp_exact is not None and getattr(resp_exact, "hits", None):
|
||||
|
|
@ -314,13 +341,23 @@ def process_query(
|
|||
.prefetch_related("images")
|
||||
}
|
||||
if uuids_by_index.get("brands"):
|
||||
brands_by_uuid = {str(b.uuid): b for b in Brand.objects.filter(uuid__in=uuids_by_index["brands"])}
|
||||
brands_by_uuid = {
|
||||
str(b.uuid): b
|
||||
for b in Brand.objects.filter(uuid__in=uuids_by_index["brands"])
|
||||
}
|
||||
if uuids_by_index.get("categories"):
|
||||
cats_by_uuid = {str(c.uuid): c for c in Category.objects.filter(uuid__in=uuids_by_index["categories"])}
|
||||
cats_by_uuid = {
|
||||
str(c.uuid): c
|
||||
for c in Category.objects.filter(
|
||||
uuid__in=uuids_by_index["categories"]
|
||||
)
|
||||
}
|
||||
|
||||
for hit in hit_cache:
|
||||
obj_uuid = getattr(hit, "uuid", None) or hit.meta.id
|
||||
obj_name = getattr(hit, "name", None) or getattr(hit, "title", None) or "N/A"
|
||||
obj_name = (
|
||||
getattr(hit, "name", None) or getattr(hit, "title", None) or "N/A"
|
||||
)
|
||||
obj_slug = getattr(hit, "slug", "") or (
|
||||
slugify(obj_name) if hit.meta.index in {"brands", "categories"} else ""
|
||||
)
|
||||
|
|
@ -353,8 +390,12 @@ def process_query(
|
|||
if idx == "products":
|
||||
hit_result["rating_debug"] = getattr(hit, "rating", 0)
|
||||
hit_result["total_orders_debug"] = getattr(hit, "total_orders", 0)
|
||||
hit_result["brand_priority_debug"] = getattr(hit, "brand_priority", 0)
|
||||
hit_result["category_priority_debug"] = getattr(hit, "category_priority", 0)
|
||||
hit_result["brand_priority_debug"] = getattr(
|
||||
hit, "brand_priority", 0
|
||||
)
|
||||
hit_result["category_priority_debug"] = getattr(
|
||||
hit, "category_priority", 0
|
||||
)
|
||||
if idx in ("brands", "categories"):
|
||||
hit_result["priority_debug"] = getattr(hit, "priority", 0)
|
||||
|
||||
|
|
@ -402,14 +443,22 @@ class ActiveOnlyMixin:
|
|||
COMMON_ANALYSIS = {
|
||||
"char_filter": {
|
||||
"icu_nfkc_cf": {"type": "icu_normalizer", "name": "nfkc_cf"},
|
||||
"strip_ws_punct": {"type": "pattern_replace", "pattern": "[\\s\\p{Punct}]+", "replacement": ""},
|
||||
"strip_ws_punct": {
|
||||
"type": "pattern_replace",
|
||||
"pattern": "[\\s\\p{Punct}]+",
|
||||
"replacement": "",
|
||||
},
|
||||
},
|
||||
"filter": {
|
||||
"edge_ngram_filter": {"type": "edge_ngram", "min_gram": 1, "max_gram": 20},
|
||||
"ngram_filter": {"type": "ngram", "min_gram": 2, "max_gram": 20},
|
||||
"cjk_bigram": {"type": "cjk_bigram"},
|
||||
"icu_folding": {"type": "icu_folding"},
|
||||
"double_metaphone": {"type": "phonetic", "encoder": "double_metaphone", "replace": False},
|
||||
"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"},
|
||||
|
|
@ -520,9 +569,13 @@ def add_multilang_fields(cls: Any) -> None:
|
|||
copy_to="name",
|
||||
fields={
|
||||
"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"),
|
||||
"translit": fields.TextField(analyzer="translit_index", search_analyzer="translit_query"),
|
||||
"translit": fields.TextField(
|
||||
analyzer="translit_index", search_analyzer="translit_query"
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
@ -542,9 +595,13 @@ def add_multilang_fields(cls: Any) -> None:
|
|||
copy_to="description",
|
||||
fields={
|
||||
"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"),
|
||||
"translit": fields.TextField(analyzer="translit_index", search_analyzer="translit_query"),
|
||||
"translit": fields.TextField(
|
||||
analyzer="translit_index", search_analyzer="translit_query"
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
@ -582,7 +639,9 @@ def process_system_query(
|
|||
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")
|
||||
f.replace("ngram^6", "ngram^8")
|
||||
.replace("ngram^5", "ngram^7")
|
||||
.replace("ngram^3", "ngram^4")
|
||||
for f in fields_all
|
||||
]
|
||||
|
||||
|
|
@ -601,7 +660,11 @@ def process_system_query(
|
|||
results: dict[str, list[dict[str, Any]]] = {idx: [] for idx in indexes}
|
||||
|
||||
for idx in indexes:
|
||||
s = Search(index=[idx]).query(mm).extra(size=size_per_index, track_total_hits=False)
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@ from django_elasticsearch_dsl import Document, fields
|
|||
from django_elasticsearch_dsl.registries import registry
|
||||
from health_check.db.models import TestModel
|
||||
|
||||
from engine.core.elasticsearch import COMMON_ANALYSIS, ActiveOnlyMixin, add_multilang_fields
|
||||
from engine.core.elasticsearch import (
|
||||
COMMON_ANALYSIS,
|
||||
ActiveOnlyMixin,
|
||||
add_multilang_fields,
|
||||
)
|
||||
from engine.core.models import Brand, Category, Product
|
||||
|
||||
|
||||
|
|
@ -15,10 +19,16 @@ class BaseDocument(Document): # type: ignore [misc]
|
|||
analyzer="standard",
|
||||
fields={
|
||||
"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"),
|
||||
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
|
||||
"translit": fields.TextField(analyzer="translit_index", search_analyzer="translit_query"),
|
||||
"auto": fields.TextField(
|
||||
analyzer="autocomplete", search_analyzer="autocomplete_search"
|
||||
),
|
||||
"translit": fields.TextField(
|
||||
analyzer="translit_index", search_analyzer="translit_query"
|
||||
),
|
||||
"ci": fields.TextField(analyzer="name_exact", search_analyzer="name_exact"),
|
||||
},
|
||||
)
|
||||
|
|
@ -27,10 +37,16 @@ class BaseDocument(Document): # type: ignore [misc]
|
|||
analyzer="standard",
|
||||
fields={
|
||||
"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"),
|
||||
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
|
||||
"translit": fields.TextField(analyzer="translit_index", search_analyzer="translit_query"),
|
||||
"auto": fields.TextField(
|
||||
analyzer="autocomplete", search_analyzer="autocomplete_search"
|
||||
),
|
||||
"translit": fields.TextField(
|
||||
analyzer="translit_index", search_analyzer="translit_query"
|
||||
),
|
||||
},
|
||||
)
|
||||
slug = fields.KeywordField(attr="slug")
|
||||
|
|
@ -70,10 +86,16 @@ class ProductDocument(ActiveOnlyMixin, BaseDocument):
|
|||
analyzer="standard",
|
||||
fields={
|
||||
"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"),
|
||||
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
|
||||
"translit": fields.TextField(analyzer="translit_index", search_analyzer="translit_query"),
|
||||
"auto": fields.TextField(
|
||||
analyzer="autocomplete", search_analyzer="autocomplete_search"
|
||||
),
|
||||
"translit": fields.TextField(
|
||||
analyzer="translit_index", search_analyzer="translit_query"
|
||||
),
|
||||
},
|
||||
)
|
||||
category_name = fields.TextField(
|
||||
|
|
@ -81,10 +103,16 @@ class ProductDocument(ActiveOnlyMixin, BaseDocument):
|
|||
analyzer="standard",
|
||||
fields={
|
||||
"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"),
|
||||
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
|
||||
"translit": fields.TextField(analyzer="translit_index", search_analyzer="translit_query"),
|
||||
"auto": fields.TextField(
|
||||
analyzer="autocomplete", search_analyzer="autocomplete_search"
|
||||
),
|
||||
"translit": fields.TextField(
|
||||
analyzer="translit_index", search_analyzer="translit_query"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -93,8 +121,12 @@ class ProductDocument(ActiveOnlyMixin, BaseDocument):
|
|||
normalizer="lc_norm",
|
||||
fields={
|
||||
"raw": fields.KeywordField(normalizer="lc_norm"),
|
||||
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
|
||||
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
|
||||
"ngram": fields.TextField(
|
||||
analyzer="name_ngram", search_analyzer="icu_query"
|
||||
),
|
||||
"auto": fields.TextField(
|
||||
analyzer="autocomplete", search_analyzer="autocomplete_search"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -103,8 +135,12 @@ class ProductDocument(ActiveOnlyMixin, BaseDocument):
|
|||
normalizer="lc_norm",
|
||||
fields={
|
||||
"raw": fields.KeywordField(normalizer="lc_norm"),
|
||||
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="icu_query"),
|
||||
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
|
||||
"ngram": fields.TextField(
|
||||
analyzer="name_ngram", search_analyzer="icu_query"
|
||||
),
|
||||
"auto": fields.TextField(
|
||||
analyzer="autocomplete", search_analyzer="autocomplete_search"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,16 @@ from graphene import Context
|
|||
from rest_framework.request import Request
|
||||
|
||||
from engine.core.elasticsearch import process_query
|
||||
from engine.core.models import Address, Brand, Category, Feedback, Order, Product, Stock, Wishlist
|
||||
from engine.core.models import (
|
||||
Address,
|
||||
Brand,
|
||||
Category,
|
||||
Feedback,
|
||||
Order,
|
||||
Product,
|
||||
Stock,
|
||||
Wishlist,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -69,19 +78,31 @@ class ProductFilter(FilterSet): # type: ignore [misc]
|
|||
search = CharFilter(field_name="name", method="search_products", label=_("Search"))
|
||||
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID"))
|
||||
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)")
|
||||
categories_slugs = CaseInsensitiveListFilter(field_name="category__slug", label=_("Categories Slugs"))
|
||||
categories_slugs = CaseInsensitiveListFilter(
|
||||
field_name="category__slug", label=_("Categories Slugs")
|
||||
)
|
||||
tags = CaseInsensitiveListFilter(field_name="tags__tag_name", label=_("Tags"))
|
||||
min_price = NumberFilter(field_name="stocks__price", lookup_expr="gte", label=_("Min Price"))
|
||||
max_price = NumberFilter(field_name="stocks__price", lookup_expr="lte", label=_("Max Price"))
|
||||
min_price = NumberFilter(
|
||||
field_name="stocks__price", lookup_expr="gte", label=_("Min Price")
|
||||
)
|
||||
max_price = NumberFilter(
|
||||
field_name="stocks__price", lookup_expr="lte", label=_("Max Price")
|
||||
)
|
||||
is_active = BooleanFilter(field_name="is_active", label=_("Is Active"))
|
||||
brand = CharFilter(field_name="brand__name", lookup_expr="iexact", label=_("Brand"))
|
||||
attributes = CharFilter(method="filter_attributes", label=_("Attributes"))
|
||||
quantity = NumberFilter(field_name="stocks__quantity", lookup_expr="gt", label=_("Quantity"))
|
||||
quantity = NumberFilter(
|
||||
field_name="stocks__quantity", lookup_expr="gt", label=_("Quantity")
|
||||
)
|
||||
slug = CharFilter(field_name="slug", lookup_expr="exact", label=_("Slug"))
|
||||
is_digital = BooleanFilter(field_name="is_digital", label=_("Is Digital"))
|
||||
include_subcategories = BooleanFilter(method="filter_include_flag", label=_("Include sub-categories"))
|
||||
include_subcategories = BooleanFilter(
|
||||
method="filter_include_flag", label=_("Include sub-categories")
|
||||
)
|
||||
include_personal_ordered = BooleanFilter(
|
||||
method="filter_include_personal_ordered",
|
||||
label=_("Include personal ordered"),
|
||||
|
|
@ -161,7 +182,9 @@ class ProductFilter(FilterSet): # type: ignore [misc]
|
|||
)
|
||||
)
|
||||
|
||||
def search_products(self, queryset: QuerySet[Product], name: str, value: str) -> QuerySet[Product]:
|
||||
def search_products(
|
||||
self, queryset: QuerySet[Product], name: str, value: str
|
||||
) -> QuerySet[Product]:
|
||||
if not value:
|
||||
return queryset
|
||||
|
||||
|
|
@ -173,23 +196,35 @@ class ProductFilter(FilterSet): # type: ignore [misc]
|
|||
# Preserve ES order using a CASE expression
|
||||
when_statements = [When(uuid=u, then=pos) for pos, u in enumerate(uuids)]
|
||||
queryset = queryset.filter(uuid__in=uuids).annotate(
|
||||
es_rank=Case(*when_statements, default=Value(9999), output_field=IntegerField())
|
||||
es_rank=Case(
|
||||
*when_statements, default=Value(9999), output_field=IntegerField()
|
||||
)
|
||||
)
|
||||
# Mark that ES ranking is applied, qs() will order appropriately
|
||||
self._es_rank_applied = True
|
||||
return queryset
|
||||
|
||||
def filter_include_flag(self, queryset: QuerySet[Product], name: str, value: str) -> QuerySet[Product]:
|
||||
def filter_include_flag(
|
||||
self, queryset: QuerySet[Product], name: str, value: str
|
||||
) -> QuerySet[Product]:
|
||||
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
|
||||
|
||||
def filter_include_personal_ordered(self, queryset: QuerySet[Product], name: str, value: str) -> QuerySet[Product]:
|
||||
def filter_include_personal_ordered(
|
||||
self, queryset: QuerySet[Product], name: str, value: str
|
||||
) -> QuerySet[Product]:
|
||||
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
|
||||
|
||||
def filter_attributes(self, queryset: QuerySet[Product], name: str, value: str) -> QuerySet[Product]:
|
||||
def filter_attributes(
|
||||
self, queryset: QuerySet[Product], name: str, value: str
|
||||
) -> QuerySet[Product]:
|
||||
if not value:
|
||||
return queryset
|
||||
|
||||
|
|
@ -251,7 +286,9 @@ class ProductFilter(FilterSet): # type: ignore [misc]
|
|||
|
||||
return queryset
|
||||
|
||||
def filter_category(self, queryset: QuerySet[Product], name: str, value: str) -> QuerySet[Product]:
|
||||
def filter_category(
|
||||
self, queryset: QuerySet[Product], name: str, value: str
|
||||
) -> QuerySet[Product]:
|
||||
if not value:
|
||||
return queryset
|
||||
|
||||
|
|
@ -313,7 +350,11 @@ class ProductFilter(FilterSet): # type: ignore [misc]
|
|||
)
|
||||
)
|
||||
|
||||
requested = [part.strip() for part in ordering_param.split(",") if part.strip()] if ordering_param else []
|
||||
requested = (
|
||||
[part.strip() for part in ordering_param.split(",") if part.strip()]
|
||||
if ordering_param
|
||||
else []
|
||||
)
|
||||
mapped_requested: list[str] = []
|
||||
|
||||
for part in requested:
|
||||
|
|
@ -327,7 +368,11 @@ class ProductFilter(FilterSet): # type: ignore [misc]
|
|||
mapped_requested.append("?")
|
||||
continue
|
||||
|
||||
if key in {"personal_orders_only", "personal_order_only", "personal_order_tail"}:
|
||||
if key in {
|
||||
"personal_orders_only",
|
||||
"personal_order_only",
|
||||
"personal_order_tail",
|
||||
}:
|
||||
continue
|
||||
|
||||
mapped_requested.append(f"-{key}" if desc else key)
|
||||
|
|
@ -353,12 +398,20 @@ class OrderFilter(FilterSet): # type: ignore [misc]
|
|||
label=_("Search (ID, product name or part number)"),
|
||||
)
|
||||
|
||||
min_buy_time = DateTimeFilter(field_name="buy_time", lookup_expr="gte", label=_("Bought after (inclusive)"))
|
||||
max_buy_time = DateTimeFilter(field_name="buy_time", lookup_expr="lte", label=_("Bought before (inclusive)"))
|
||||
min_buy_time = DateTimeFilter(
|
||||
field_name="buy_time", lookup_expr="gte", label=_("Bought after (inclusive)")
|
||||
)
|
||||
max_buy_time = DateTimeFilter(
|
||||
field_name="buy_time", lookup_expr="lte", label=_("Bought before (inclusive)")
|
||||
)
|
||||
|
||||
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
|
||||
user_email = CharFilter(field_name="user__email", lookup_expr="iexact", label=_("User email"))
|
||||
user = UUIDFilter(field_name="user__uuid", lookup_expr="exact", label=_("User UUID"))
|
||||
user_email = CharFilter(
|
||||
field_name="user__email", lookup_expr="iexact", label=_("User email")
|
||||
)
|
||||
user = UUIDFilter(
|
||||
field_name="user__uuid", lookup_expr="exact", label=_("User UUID")
|
||||
)
|
||||
status = CharFilter(field_name="status", lookup_expr="icontains", label=_("Status"))
|
||||
human_readable_id = CharFilter(
|
||||
field_name="human_readable_id",
|
||||
|
|
@ -404,8 +457,12 @@ class OrderFilter(FilterSet): # type: ignore [misc]
|
|||
|
||||
class WishlistFilter(FilterSet): # type: ignore [misc]
|
||||
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
|
||||
user_email = CharFilter(field_name="user__email", lookup_expr="iexact", label=_("User email"))
|
||||
user = UUIDFilter(field_name="user__uuid", lookup_expr="exact", label=_("User UUID"))
|
||||
user_email = CharFilter(
|
||||
field_name="user__email", lookup_expr="iexact", label=_("User email")
|
||||
)
|
||||
user = UUIDFilter(
|
||||
field_name="user__uuid", lookup_expr="exact", label=_("User UUID")
|
||||
)
|
||||
|
||||
order_by = OrderingFilter(
|
||||
fields=(
|
||||
|
|
@ -423,7 +480,9 @@ class WishlistFilter(FilterSet): # type: ignore [misc]
|
|||
|
||||
# noinspection PyUnusedLocal
|
||||
class CategoryFilter(FilterSet): # type: ignore [misc]
|
||||
search = CharFilter(field_name="name", method="search_categories", label=_("Search"))
|
||||
search = CharFilter(
|
||||
field_name="name", method="search_categories", label=_("Search")
|
||||
)
|
||||
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
|
||||
name = CharFilter(lookup_expr="icontains", label=_("Name"))
|
||||
parent_uuid = CharFilter(method="filter_parent_uuid", label=_("Parent"))
|
||||
|
|
@ -451,15 +510,24 @@ class CategoryFilter(FilterSet): # type: ignore [misc]
|
|||
"whole",
|
||||
]
|
||||
|
||||
def search_categories(self, queryset: QuerySet[Category], name: str, value: str) -> QuerySet[Category]:
|
||||
def search_categories(
|
||||
self, queryset: QuerySet[Category], name: str, value: str
|
||||
) -> QuerySet[Category]:
|
||||
if not value:
|
||||
return queryset
|
||||
|
||||
uuids = [category.get("uuid") for category in process_query(query=value, indexes=("categories",))["categories"]] # type: ignore
|
||||
uuids = [
|
||||
category.get("uuid")
|
||||
for category in process_query(query=value, indexes=("categories",))[
|
||||
"categories"
|
||||
]
|
||||
] # type: ignore
|
||||
|
||||
return queryset.filter(uuid__in=uuids)
|
||||
|
||||
def filter_order_by(self, queryset: QuerySet[Category], name: str, value: str) -> QuerySet[Category]:
|
||||
def filter_order_by(
|
||||
self, queryset: QuerySet[Category], name: str, value: str
|
||||
) -> QuerySet[Category]:
|
||||
if not value:
|
||||
return queryset
|
||||
|
||||
|
|
@ -505,7 +573,9 @@ class CategoryFilter(FilterSet): # type: ignore [misc]
|
|||
if depth <= 0:
|
||||
return None
|
||||
|
||||
children_qs = Category.objects.all().order_by(order_expression, "tree_id", "lft")
|
||||
children_qs = Category.objects.all().order_by(
|
||||
order_expression, "tree_id", "lft"
|
||||
)
|
||||
nested_prefetch = build_ordered_prefetch(depth - 1)
|
||||
|
||||
if nested_prefetch:
|
||||
|
|
@ -521,7 +591,9 @@ class CategoryFilter(FilterSet): # type: ignore [misc]
|
|||
|
||||
return qs
|
||||
|
||||
def filter_whole_categories(self, queryset: QuerySet[Category], name: str, value: str) -> QuerySet[Category]:
|
||||
def filter_whole_categories(
|
||||
self, queryset: QuerySet[Category], name: str, value: str
|
||||
) -> QuerySet[Category]:
|
||||
has_own_products = Exists(Product.objects.filter(category=OuterRef("pk")))
|
||||
has_desc_products = Exists(
|
||||
Product.objects.filter(
|
||||
|
|
@ -554,7 +626,9 @@ class BrandFilter(FilterSet): # type: ignore [misc]
|
|||
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
|
||||
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"))
|
||||
categories = CaseInsensitiveListFilter(
|
||||
field_name="categories__uuid", lookup_expr="exact", label=_("Categories")
|
||||
)
|
||||
|
||||
order_by = OrderingFilter(
|
||||
fields=(
|
||||
|
|
@ -570,11 +644,16 @@ class BrandFilter(FilterSet): # type: ignore [misc]
|
|||
model = Brand
|
||||
fields = ["uuid", "name", "slug", "priority"]
|
||||
|
||||
def search_brands(self, queryset: QuerySet[Brand], name: str, value: str) -> QuerySet[Brand]:
|
||||
def search_brands(
|
||||
self, queryset: QuerySet[Brand], name: str, value: str
|
||||
) -> QuerySet[Brand]:
|
||||
if not value:
|
||||
return queryset
|
||||
|
||||
uuids = [brand.get("uuid") for brand in process_query(query=value, indexes=("brands",))["brands"]] # type: ignore
|
||||
uuids = [
|
||||
brand.get("uuid")
|
||||
for brand in process_query(query=value, indexes=("brands",))["brands"]
|
||||
] # type: ignore
|
||||
|
||||
return queryset.filter(uuid__in=uuids)
|
||||
|
||||
|
|
@ -610,8 +689,12 @@ class FeedbackFilter(FilterSet): # type: ignore [misc]
|
|||
|
||||
class AddressFilter(FilterSet): # type: ignore [misc]
|
||||
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID"))
|
||||
user_uuid = UUIDFilter(field_name="user__uuid", lookup_expr="exact", label=_("User UUID"))
|
||||
user_email = CharFilter(field_name="user__email", lookup_expr="iexact", label=_("User email"))
|
||||
user_uuid = UUIDFilter(
|
||||
field_name="user__uuid", lookup_expr="exact", label=_("User UUID")
|
||||
)
|
||||
user_email = CharFilter(
|
||||
field_name="user__email", lookup_expr="iexact", label=_("User email")
|
||||
)
|
||||
|
||||
order_by = OrderingFilter(
|
||||
fields=(
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ class CacheOperator(BaseMutation):
|
|||
description = _("cache I/O")
|
||||
|
||||
class Arguments:
|
||||
key = String(required=True, description=_("key to look for in or set into the cache"))
|
||||
key = String(
|
||||
required=True, description=_("key to look for in or set into the cache")
|
||||
)
|
||||
data = GenericScalar(required=False, description=_("data to store in cache"))
|
||||
timeout = Int(
|
||||
required=False,
|
||||
|
|
@ -68,7 +70,9 @@ class RequestCursedURL(BaseMutation):
|
|||
try:
|
||||
data = cache.get(url, None)
|
||||
if not data:
|
||||
response = requests.get(url, headers={"content-type": "application/json"})
|
||||
response = requests.get(
|
||||
url, headers={"content-type": "application/json"}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = camelize(response.json())
|
||||
cache.set(url, data, 86400)
|
||||
|
|
@ -97,7 +101,9 @@ class AddOrderProduct(BaseMutation):
|
|||
if not (user.has_perm("core.add_orderproduct") or user == order.user):
|
||||
raise PermissionDenied(permission_denied_message)
|
||||
|
||||
order = order.add_product(product_uuid=product_uuid, attributes=format_attributes(attributes))
|
||||
order = order.add_product(
|
||||
product_uuid=product_uuid, attributes=format_attributes(attributes)
|
||||
)
|
||||
|
||||
return AddOrderProduct(order=order)
|
||||
except Order.DoesNotExist as dne:
|
||||
|
|
@ -124,7 +130,9 @@ class RemoveOrderProduct(BaseMutation):
|
|||
if not (user.has_perm("core.change_orderproduct") or user == order.user):
|
||||
raise PermissionDenied(permission_denied_message)
|
||||
|
||||
order = order.remove_product(product_uuid=product_uuid, attributes=format_attributes(attributes))
|
||||
order = order.remove_product(
|
||||
product_uuid=product_uuid, attributes=format_attributes(attributes)
|
||||
)
|
||||
|
||||
return RemoveOrderProduct(order=order)
|
||||
except Order.DoesNotExist as dne:
|
||||
|
|
@ -208,7 +216,11 @@ class BuyOrder(BaseMutation):
|
|||
chosen_products=None,
|
||||
): # type: ignore [override]
|
||||
if not any([order_uuid, order_hr_id]) or all([order_uuid, order_hr_id]):
|
||||
raise BadRequest(_("please provide either order_uuid or order_hr_id - mutually exclusive"))
|
||||
raise BadRequest(
|
||||
_(
|
||||
"please provide either order_uuid or order_hr_id - mutually exclusive"
|
||||
)
|
||||
)
|
||||
user = info.context.user
|
||||
try:
|
||||
order = None
|
||||
|
|
@ -233,7 +245,11 @@ class BuyOrder(BaseMutation):
|
|||
case "<class 'engine.core.models.Order'>":
|
||||
return BuyOrder(order=instance)
|
||||
case _:
|
||||
raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}"))
|
||||
raise TypeError(
|
||||
_(
|
||||
f"wrong type came from order.buy() method: {type(instance)!s}"
|
||||
)
|
||||
)
|
||||
|
||||
except Order.DoesNotExist as dne:
|
||||
raise Http404(_(f"order {order_uuid} not found")) from dne
|
||||
|
|
@ -262,7 +278,11 @@ class BulkOrderAction(BaseMutation):
|
|||
order_hr_id=None,
|
||||
): # type: ignore [override]
|
||||
if not any([order_uuid, order_hr_id]) or all([order_uuid, order_hr_id]):
|
||||
raise BadRequest(_("please provide either order_uuid or order_hr_id - mutually exclusive"))
|
||||
raise BadRequest(
|
||||
_(
|
||||
"please provide either order_uuid or order_hr_id - mutually exclusive"
|
||||
)
|
||||
)
|
||||
user = info.context.user
|
||||
try:
|
||||
order = None
|
||||
|
|
@ -491,14 +511,20 @@ class BuyWishlist(BaseMutation):
|
|||
):
|
||||
order.add_product(product_uuid=product.pk)
|
||||
|
||||
instance = order.buy(force_balance=force_balance, force_payment=force_payment)
|
||||
instance = order.buy(
|
||||
force_balance=force_balance, force_payment=force_payment
|
||||
)
|
||||
match str(type(instance)):
|
||||
case "<class 'engine.payments.models.Transaction'>":
|
||||
return BuyWishlist(transaction=instance)
|
||||
case "<class 'engine.core.models.Order'>":
|
||||
return BuyWishlist(order=instance)
|
||||
case _:
|
||||
raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}"))
|
||||
raise TypeError(
|
||||
_(
|
||||
f"wrong type came from order.buy() method: {type(instance)!s}"
|
||||
)
|
||||
)
|
||||
|
||||
except Wishlist.DoesNotExist as dne:
|
||||
raise Http404(_(f"wishlist {wishlist_uuid} not found")) from dne
|
||||
|
|
@ -513,7 +539,9 @@ class BuyProduct(BaseMutation):
|
|||
product_uuid = UUID(required=True)
|
||||
attributes = String(
|
||||
required=False,
|
||||
description=_("please send the attributes as the string formatted like attr1=value1,attr2=value2"),
|
||||
description=_(
|
||||
"please send the attributes as the string formatted like attr1=value1,attr2=value2"
|
||||
),
|
||||
)
|
||||
force_balance = Boolean(required=False)
|
||||
force_payment = Boolean(required=False)
|
||||
|
|
@ -532,7 +560,9 @@ class BuyProduct(BaseMutation):
|
|||
): # type: ignore [override]
|
||||
user = info.context.user
|
||||
order = Order.objects.create(user=user, status="MOMENTAL")
|
||||
order.add_product(product_uuid=product_uuid, attributes=format_attributes(attributes))
|
||||
order.add_product(
|
||||
product_uuid=product_uuid, attributes=format_attributes(attributes)
|
||||
)
|
||||
instance = order.buy(force_balance=force_balance, force_payment=force_payment)
|
||||
match str(type(instance)):
|
||||
case "<class 'engine.payments.models.Transaction'>":
|
||||
|
|
@ -540,7 +570,9 @@ class BuyProduct(BaseMutation):
|
|||
case "<class 'engine.core.models.Order'>":
|
||||
return BuyProduct(order=instance)
|
||||
case _:
|
||||
raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}"))
|
||||
raise TypeError(
|
||||
_(f"wrong type came from order.buy() method: {type(instance)!s}")
|
||||
)
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal,PyTypeChecker
|
||||
|
|
@ -566,7 +598,9 @@ class FeedbackProductAction(BaseMutation):
|
|||
feedback = None
|
||||
match action:
|
||||
case "add":
|
||||
feedback = order_product.do_feedback(comment=comment, rating=rating, action="add")
|
||||
feedback = order_product.do_feedback(
|
||||
comment=comment, rating=rating, action="add"
|
||||
)
|
||||
case "remove":
|
||||
feedback = order_product.do_feedback(action="remove")
|
||||
case _:
|
||||
|
|
@ -579,7 +613,9 @@ class FeedbackProductAction(BaseMutation):
|
|||
# noinspection PyUnusedLocal,PyTypeChecker
|
||||
class CreateAddress(BaseMutation):
|
||||
class Arguments:
|
||||
raw_data = String(required=True, description=_("original address string provided by the user"))
|
||||
raw_data = String(
|
||||
required=True, description=_("original address string provided by the user")
|
||||
)
|
||||
|
||||
address = Field(AddressType)
|
||||
|
||||
|
|
|
|||
|
|
@ -119,7 +119,15 @@ class BrandType(DjangoObjectType): # type: ignore [misc]
|
|||
class Meta:
|
||||
model = Brand
|
||||
interfaces = (relay.Node,)
|
||||
fields = ("uuid", "categories", "name", "description", "big_logo", "small_logo", "slug")
|
||||
fields = (
|
||||
"uuid",
|
||||
"categories",
|
||||
"name",
|
||||
"description",
|
||||
"big_logo",
|
||||
"small_logo",
|
||||
"slug",
|
||||
)
|
||||
filter_fields = ["uuid", "name"]
|
||||
description = _("brands")
|
||||
|
||||
|
|
@ -129,12 +137,20 @@ class BrandType(DjangoObjectType): # type: ignore [misc]
|
|||
return self.categories.filter(is_active=True)
|
||||
|
||||
def resolve_big_logo(self: Brand, info) -> str | None:
|
||||
return info.context.build_absolute_uri(self.big_logo.url) if self.big_logo else ""
|
||||
return (
|
||||
info.context.build_absolute_uri(self.big_logo.url) if self.big_logo else ""
|
||||
)
|
||||
|
||||
def resolve_small_logo(self: Brand, info) -> str | None:
|
||||
return info.context.build_absolute_uri(self.small_logo.url) if self.small_logo else ""
|
||||
return (
|
||||
info.context.build_absolute_uri(self.small_logo.url)
|
||||
if self.small_logo
|
||||
else ""
|
||||
)
|
||||
|
||||
def resolve_seo_meta(self: Brand, info) -> dict[str, str | list[Any] | dict[str, str] | None]:
|
||||
def resolve_seo_meta(
|
||||
self: Brand, info
|
||||
) -> dict[str, str | list[Any] | dict[str, str] | None]:
|
||||
lang = graphene_current_lang()
|
||||
base = f"https://{settings.BASE_DOMAIN}"
|
||||
canonical = f"{base}/{lang}/brand/{self.slug}"
|
||||
|
|
@ -154,7 +170,12 @@ class BrandType(DjangoObjectType): # type: ignore [misc]
|
|||
"url": canonical,
|
||||
"image": logo_url or "",
|
||||
}
|
||||
tw = {"card": "summary_large_image", "title": title, "description": description, "image": logo_url or ""}
|
||||
tw = {
|
||||
"card": "summary_large_image",
|
||||
"title": title,
|
||||
"description": description,
|
||||
"image": logo_url or "",
|
||||
}
|
||||
|
||||
crumbs = [("Home", f"{base}/"), (self.name, canonical)]
|
||||
|
||||
|
|
@ -196,14 +217,22 @@ class CategoryType(DjangoObjectType): # type: ignore [misc]
|
|||
markup_percent = Float(required=False, description=_("markup percentage"))
|
||||
filterable_attributes = List(
|
||||
NonNull(FilterableAttributeType),
|
||||
description=_("which attributes and values can be used for filtering this category."),
|
||||
description=_(
|
||||
"which attributes and values can be used for filtering this category."
|
||||
),
|
||||
)
|
||||
min_max_prices = Field(
|
||||
NonNull(MinMaxPriceType),
|
||||
description=_("minimum and maximum prices for products in this category, if available."),
|
||||
description=_(
|
||||
"minimum and maximum prices for products in this category, if available."
|
||||
),
|
||||
)
|
||||
tags = DjangoFilterConnectionField(
|
||||
lambda: CategoryTagType, description=_("tags for this category")
|
||||
)
|
||||
products = DjangoFilterConnectionField(
|
||||
lambda: ProductType, description=_("products in this category")
|
||||
)
|
||||
tags = DjangoFilterConnectionField(lambda: CategoryTagType, description=_("tags for this category"))
|
||||
products = DjangoFilterConnectionField(lambda: ProductType, description=_("products in this category"))
|
||||
seo_meta = Field(SEOMetaType, description=_("SEO Meta snapshot"))
|
||||
|
||||
class Meta:
|
||||
|
|
@ -253,7 +282,9 @@ class CategoryType(DjangoObjectType): # type: ignore [misc]
|
|||
)
|
||||
min_max_prices["min_price"] = price_aggregation.get("min_price", 0.0)
|
||||
min_max_prices["max_price"] = price_aggregation.get("max_price", 0.0)
|
||||
cache.set(key=f"{self.name}_min_max_prices", value=min_max_prices, timeout=86400)
|
||||
cache.set(
|
||||
key=f"{self.name}_min_max_prices", value=min_max_prices, timeout=86400
|
||||
)
|
||||
|
||||
return {
|
||||
"min_price": min_max_prices["min_price"],
|
||||
|
|
@ -267,7 +298,11 @@ class CategoryType(DjangoObjectType): # type: ignore [misc]
|
|||
title = f"{self.name} | {settings.PROJECT_NAME}"
|
||||
description = (self.description or "")[:180]
|
||||
|
||||
og_image = graphene_abs(info.context, self.image.url) if getattr(self, "image", None) else ""
|
||||
og_image = (
|
||||
graphene_abs(info.context, self.image.url)
|
||||
if getattr(self, "image", None)
|
||||
else ""
|
||||
)
|
||||
|
||||
og = {
|
||||
"title": title,
|
||||
|
|
@ -276,14 +311,24 @@ class CategoryType(DjangoObjectType): # type: ignore [misc]
|
|||
"url": canonical,
|
||||
"image": og_image,
|
||||
}
|
||||
tw = {"card": "summary_large_image", "title": title, "description": description, "image": og_image}
|
||||
tw = {
|
||||
"card": "summary_large_image",
|
||||
"title": title,
|
||||
"description": description,
|
||||
"image": og_image,
|
||||
}
|
||||
|
||||
crumbs = [("Home", f"{base}/")]
|
||||
for c in self.get_ancestors():
|
||||
crumbs.append((c.name, f"{base}/{lang}/catalog/{c.slug}"))
|
||||
crumbs.append((self.name, canonical))
|
||||
|
||||
json_ld = [org_schema(), website_schema(), breadcrumb_schema(crumbs), category_schema(self, canonical)]
|
||||
json_ld = [
|
||||
org_schema(),
|
||||
website_schema(),
|
||||
breadcrumb_schema(crumbs),
|
||||
category_schema(self, canonical),
|
||||
]
|
||||
|
||||
product_urls = []
|
||||
qs = (
|
||||
|
|
@ -356,7 +401,9 @@ class AddressType(DjangoObjectType): # type: ignore [misc]
|
|||
|
||||
class FeedbackType(DjangoObjectType): # type: ignore [misc]
|
||||
comment = String(description=_("comment"))
|
||||
rating = Int(description=_("rating value from 1 to 10, inclusive, or 0 if not set."))
|
||||
rating = Int(
|
||||
description=_("rating value from 1 to 10, inclusive, or 0 if not set.")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Feedback
|
||||
|
|
@ -369,7 +416,9 @@ class FeedbackType(DjangoObjectType): # type: ignore [misc]
|
|||
class OrderProductType(DjangoObjectType): # type: ignore [misc]
|
||||
attributes = GenericScalar(description=_("attributes"))
|
||||
notifications = GenericScalar(description=_("notifications"))
|
||||
download_url = String(description=_("download url for this order product if applicable"))
|
||||
download_url = String(
|
||||
description=_("download url for this order product if applicable")
|
||||
)
|
||||
feedback = Field(lambda: FeedbackType, description=_("feedback"))
|
||||
|
||||
class Meta:
|
||||
|
|
@ -409,14 +458,18 @@ class OrderType(DjangoObjectType): # type: ignore [misc]
|
|||
billing_address = Field(AddressType, description=_("billing address"))
|
||||
shipping_address = Field(
|
||||
AddressType,
|
||||
description=_("shipping address for this order, leave blank if same as billing address or if not applicable"),
|
||||
description=_(
|
||||
"shipping address for this order, leave blank if same as billing address or if not applicable"
|
||||
),
|
||||
)
|
||||
total_price = Float(description=_("total price of this order"))
|
||||
total_quantity = Int(description=_("total quantity of products in order"))
|
||||
is_whole_digital = Float(description=_("are all products in the order digital"))
|
||||
attributes = GenericScalar(description=_("attributes"))
|
||||
notifications = GenericScalar(description=_("notifications"))
|
||||
payments_transactions = Field(TransactionType, description=_("transactions for this order"))
|
||||
payments_transactions = Field(
|
||||
TransactionType, description=_("transactions for this order")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
|
|
@ -474,13 +527,17 @@ class ProductType(DjangoObjectType): # type: ignore [misc]
|
|||
images = DjangoFilterConnectionField(ProductImageType, description=_("images"))
|
||||
feedbacks = DjangoFilterConnectionField(FeedbackType, description=_("feedbacks"))
|
||||
brand = Field(BrandType, description=_("brand"))
|
||||
attribute_groups = DjangoFilterConnectionField(AttributeGroupType, description=_("attribute groups"))
|
||||
attribute_groups = DjangoFilterConnectionField(
|
||||
AttributeGroupType, description=_("attribute groups")
|
||||
)
|
||||
price = Float(description=_("price"))
|
||||
quantity = Float(description=_("quantity"))
|
||||
feedbacks_count = Int(description=_("number of feedbacks"))
|
||||
personal_orders_only = Boolean(description=_("only available for personal orders"))
|
||||
seo_meta = Field(SEOMetaType, description=_("SEO Meta snapshot"))
|
||||
rating = Float(description=_("rating value from 1 to 10, inclusive, or 0 if not set."))
|
||||
rating = Float(
|
||||
description=_("rating value from 1 to 10, inclusive, or 0 if not set.")
|
||||
)
|
||||
discount_price = Float(description=_("discount price"))
|
||||
|
||||
class Meta:
|
||||
|
|
@ -524,7 +581,9 @@ class ProductType(DjangoObjectType): # type: ignore [misc]
|
|||
def resolve_attribute_groups(self: Product, info):
|
||||
info.context._product_uuid = self.uuid
|
||||
|
||||
return AttributeGroup.objects.filter(attributes__values__product=self).distinct()
|
||||
return AttributeGroup.objects.filter(
|
||||
attributes__values__product=self
|
||||
).distinct()
|
||||
|
||||
def resolve_quantity(self: Product, _info) -> int:
|
||||
return self.quantity or 0
|
||||
|
|
@ -549,7 +608,12 @@ class ProductType(DjangoObjectType): # type: ignore [misc]
|
|||
"url": canonical,
|
||||
"image": og_image,
|
||||
}
|
||||
tw = {"card": "summary_large_image", "title": title, "description": description, "image": og_image}
|
||||
tw = {
|
||||
"card": "summary_large_image",
|
||||
"title": title,
|
||||
"description": description,
|
||||
"image": og_image,
|
||||
}
|
||||
|
||||
crumbs = [("Home", f"{base}/")]
|
||||
if self.category:
|
||||
|
|
@ -611,14 +675,20 @@ class PromoCodeType(DjangoObjectType): # type: ignore [misc]
|
|||
description = _("promocodes")
|
||||
|
||||
def resolve_discount(self: PromoCode, _info) -> float:
|
||||
return float(self.discount_percent) if self.discount_percent else float(self.discount_amount) # type: ignore [arg-type]
|
||||
return (
|
||||
float(self.discount_percent)
|
||||
if self.discount_percent
|
||||
else float(self.discount_amount)
|
||||
) # type: ignore [arg-type]
|
||||
|
||||
def resolve_discount_type(self: PromoCode, _info) -> str:
|
||||
return "percent" if self.discount_percent else "amount"
|
||||
|
||||
|
||||
class PromotionType(DjangoObjectType): # type: ignore [misc]
|
||||
products = DjangoFilterConnectionField(ProductType, description=_("products on sale"))
|
||||
products = DjangoFilterConnectionField(
|
||||
ProductType, description=_("products on sale")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Promotion
|
||||
|
|
@ -641,7 +711,9 @@ class StockType(DjangoObjectType): # type: ignore [misc]
|
|||
|
||||
|
||||
class WishlistType(DjangoObjectType): # type: ignore [misc]
|
||||
products = DjangoFilterConnectionField(ProductType, description=_("wishlisted products"))
|
||||
products = DjangoFilterConnectionField(
|
||||
ProductType, description=_("wishlisted products")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Wishlist
|
||||
|
|
@ -651,7 +723,9 @@ class WishlistType(DjangoObjectType): # type: ignore [misc]
|
|||
|
||||
|
||||
class ProductTagType(DjangoObjectType): # type: ignore [misc]
|
||||
product_set = DjangoFilterConnectionField(ProductType, description=_("tagged products"))
|
||||
product_set = DjangoFilterConnectionField(
|
||||
ProductType, description=_("tagged products")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ProductTag
|
||||
|
|
@ -662,7 +736,9 @@ class ProductTagType(DjangoObjectType): # type: ignore [misc]
|
|||
|
||||
|
||||
class CategoryTagType(DjangoObjectType): # type: ignore [misc]
|
||||
category_set = DjangoFilterConnectionField(CategoryType, description=_("tagged categories"))
|
||||
category_set = DjangoFilterConnectionField(
|
||||
CategoryType, description=_("tagged categories")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CategoryTag
|
||||
|
|
@ -677,7 +753,11 @@ class ConfigType(ObjectType): # type: ignore [misc]
|
|||
company_name = String(description=_("company name"))
|
||||
company_address = String(description=_("company address"))
|
||||
company_phone_number = String(description=_("company phone number"))
|
||||
email_from = String(description=_("email from, sometimes it must be used instead of host user value"))
|
||||
email_from = String(
|
||||
description=_(
|
||||
"email from, sometimes it must be used instead of host user value"
|
||||
)
|
||||
)
|
||||
email_host_user = String(description=_("email host user"))
|
||||
payment_gateway_maximum = Float(description=_("maximum amount for payment"))
|
||||
payment_gateway_minimum = Float(description=_("minimum amount for payment"))
|
||||
|
|
@ -725,9 +805,15 @@ class SearchPostsResultsType(ObjectType): # type: ignore [misc]
|
|||
|
||||
|
||||
class SearchResultsType(ObjectType): # type: ignore [misc]
|
||||
products = List(description=_("products search results"), of_type=SearchProductsResultsType)
|
||||
categories = List(description=_("products search results"), of_type=SearchCategoriesResultsType)
|
||||
brands = List(description=_("products search results"), of_type=SearchBrandsResultsType)
|
||||
products = List(
|
||||
description=_("products search results"), of_type=SearchProductsResultsType
|
||||
)
|
||||
categories = List(
|
||||
description=_("products search results"), of_type=SearchCategoriesResultsType
|
||||
)
|
||||
brands = List(
|
||||
description=_("products search results"), of_type=SearchBrandsResultsType
|
||||
)
|
||||
posts = List(description=_("posts search results"), of_type=SearchPostsResultsType)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -113,13 +113,19 @@ class Query(ObjectType):
|
|||
users = DjangoFilterConnectionField(UserType, filterset_class=UserFilter)
|
||||
addresses = DjangoFilterConnectionField(AddressType, filterset_class=AddressFilter)
|
||||
attribute_groups = DjangoFilterConnectionField(AttributeGroupType)
|
||||
categories = DjangoFilterConnectionField(CategoryType, filterset_class=CategoryFilter)
|
||||
categories = DjangoFilterConnectionField(
|
||||
CategoryType, filterset_class=CategoryFilter
|
||||
)
|
||||
vendors = DjangoFilterConnectionField(VendorType)
|
||||
feedbacks = DjangoFilterConnectionField(FeedbackType, filterset_class=FeedbackFilter)
|
||||
feedbacks = DjangoFilterConnectionField(
|
||||
FeedbackType, filterset_class=FeedbackFilter
|
||||
)
|
||||
order_products = DjangoFilterConnectionField(OrderProductType)
|
||||
product_images = DjangoFilterConnectionField(ProductImageType)
|
||||
stocks = DjangoFilterConnectionField(StockType)
|
||||
wishlists = DjangoFilterConnectionField(WishlistType, filterset_class=WishlistFilter)
|
||||
wishlists = DjangoFilterConnectionField(
|
||||
WishlistType, filterset_class=WishlistFilter
|
||||
)
|
||||
product_tags = DjangoFilterConnectionField(ProductTagType)
|
||||
category_tags = DjangoFilterConnectionField(CategoryTagType)
|
||||
promotions = DjangoFilterConnectionField(PromotionType)
|
||||
|
|
@ -137,7 +143,12 @@ class Query(ObjectType):
|
|||
|
||||
if not languages:
|
||||
languages = [
|
||||
{"code": lang[0], "name": lang[1], "flag": get_flag_by_language(lang[0])} for lang in settings.LANGUAGES
|
||||
{
|
||||
"code": lang[0],
|
||||
"name": lang[1],
|
||||
"flag": get_flag_by_language(lang[0]),
|
||||
}
|
||||
for lang in settings.LANGUAGES
|
||||
]
|
||||
cache.set("languages", languages, 60 * 60)
|
||||
|
||||
|
|
@ -152,10 +163,16 @@ class Query(ObjectType):
|
|||
def resolve_products(_parent, info, **kwargs):
|
||||
if info.context.user.is_authenticated and kwargs.get("uuid"):
|
||||
product = Product.objects.get(uuid=kwargs["uuid"])
|
||||
if product.is_active and product.brand.is_active and product.category.is_active:
|
||||
if (
|
||||
product.is_active
|
||||
and product.brand.is_active
|
||||
and product.category.is_active
|
||||
):
|
||||
info.context.user.add_to_recently_viewed(product.uuid)
|
||||
base_qs = (
|
||||
Product.objects.all().select_related("brand", "category").prefetch_related("images", "stocks")
|
||||
Product.objects.all()
|
||||
.select_related("brand", "category")
|
||||
.prefetch_related("images", "stocks")
|
||||
if info.context.user.has_perm("core.view_product")
|
||||
else Product.objects.filter(
|
||||
is_active=True,
|
||||
|
|
@ -318,7 +335,10 @@ class Query(ObjectType):
|
|||
def resolve_promocodes(_parent, info, **kwargs):
|
||||
promocodes = PromoCode.objects
|
||||
if info.context.user.has_perm("core.view_promocode"):
|
||||
return promocodes.filter(user__uuid=kwargs.get("user_uuid")) or promocodes.all()
|
||||
return (
|
||||
promocodes.filter(user__uuid=kwargs.get("user_uuid"))
|
||||
or promocodes.all()
|
||||
)
|
||||
return promocodes.filter(
|
||||
is_active=True,
|
||||
user=info.context.user,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from django.apps import apps
|
|||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from engine.core.management.commands import RootDirectory, TRANSLATABLE_APPS
|
||||
from engine.core.management.commands import TRANSLATABLE_APPS, RootDirectory
|
||||
|
||||
# Patterns to identify placeholders
|
||||
PLACEHOLDER_REGEXES = [
|
||||
|
|
@ -118,7 +118,9 @@ class Command(BaseCommand):
|
|||
|
||||
for lang in langs:
|
||||
loc = lang.replace("-", "_")
|
||||
po_path = os.path.join(app_conf.path, "locale", loc, "LC_MESSAGES", "django.po")
|
||||
po_path = os.path.join(
|
||||
app_conf.path, "locale", loc, "LC_MESSAGES", "django.po"
|
||||
)
|
||||
if not os.path.exists(po_path):
|
||||
continue
|
||||
|
||||
|
|
@ -141,7 +143,9 @@ class Command(BaseCommand):
|
|||
display = po_path.replace("/app/", root_path)
|
||||
if "\\" in root_path:
|
||||
display = display.replace("/", "\\")
|
||||
lang_issues.append(f" {display}:{line_no}: missing={sorted(missing)} extra={sorted(extra)}")
|
||||
lang_issues.append(
|
||||
f" {display}:{line_no}: missing={sorted(missing)} extra={sorted(extra)}"
|
||||
)
|
||||
|
||||
if lang_issues:
|
||||
# Header for language with issues
|
||||
|
|
@ -157,7 +161,11 @@ class Command(BaseCommand):
|
|||
self.stdout.write("")
|
||||
else:
|
||||
# No issues in any language for this app
|
||||
self.stdout.write(self.style.SUCCESS(f"App {app_conf.label} has no placeholder issues."))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"App {app_conf.label} has no placeholder issues."
|
||||
)
|
||||
)
|
||||
self.stdout.write("")
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Done scanning."))
|
||||
|
|
|
|||
|
|
@ -37,7 +37,11 @@ class Command(BaseCommand):
|
|||
|
||||
if stock_deletions:
|
||||
Stock.objects.filter(uuid__in=stock_deletions).delete()
|
||||
self.stdout.write(self.style.SUCCESS(f"Deleted {len(stock_deletions)} duplicate stock entries."))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Deleted {len(stock_deletions)} duplicate stock entries."
|
||||
)
|
||||
)
|
||||
|
||||
# 2. Clean up duplicate Category entries based on name (case-insensitive)
|
||||
category_groups = defaultdict(list)
|
||||
|
|
@ -61,7 +65,9 @@ class Command(BaseCommand):
|
|||
for duplicate in cat_list:
|
||||
if duplicate.uuid == keep_category.uuid:
|
||||
continue
|
||||
total_product_updates += Product.objects.filter(category=duplicate).update(category=keep_category)
|
||||
total_product_updates += Product.objects.filter(
|
||||
category=duplicate
|
||||
).update(category=keep_category)
|
||||
categories_to_delete.append(str(duplicate.uuid))
|
||||
|
||||
if categories_to_delete:
|
||||
|
|
@ -78,13 +84,25 @@ class Command(BaseCommand):
|
|||
count_inactive = inactive_products.count()
|
||||
if count_inactive:
|
||||
inactive_products.update(is_active=False)
|
||||
self.stdout.write(self.style.SUCCESS(f"Set {count_inactive} product(s) as inactive due to missing stocks."))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Set {count_inactive} product(s) as inactive due to missing stocks."
|
||||
)
|
||||
)
|
||||
|
||||
# 4. Delete stocks without an associated product.
|
||||
orphan_stocks = Stock.objects.filter(product__isnull=True)
|
||||
orphan_count = orphan_stocks.count()
|
||||
if orphan_count:
|
||||
orphan_stocks.delete()
|
||||
self.stdout.write(self.style.SUCCESS(f"Deleted {orphan_count} stock(s) without an associated product."))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Deleted {orphan_count} stock(s) without an associated product."
|
||||
)
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Started fetching products task in worker container without errors!"))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
"Started fetching products task in worker container without errors!"
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ import requests
|
|||
from django.apps import apps
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from engine.core.management.commands import DEEPL_TARGET_LANGUAGES_MAPPING, TRANSLATABLE_APPS, RootDirectory
|
||||
from engine.core.management.commands import (
|
||||
DEEPL_TARGET_LANGUAGES_MAPPING,
|
||||
TRANSLATABLE_APPS,
|
||||
RootDirectory,
|
||||
)
|
||||
|
||||
# Patterns to identify placeholders
|
||||
PLACEHOLDER_REGEXES = [
|
||||
|
|
@ -60,7 +64,9 @@ def load_po_sanitized(path: str) -> polib.POFile | None:
|
|||
parts = text.split("\n\n", 1)
|
||||
header = parts[0]
|
||||
rest = parts[1] if len(parts) > 1 else ""
|
||||
rest_clean = re.sub(r"^msgid \"\"\s*\nmsgstr \"\"\s*\n?", "", rest, flags=re.MULTILINE)
|
||||
rest_clean = re.sub(
|
||||
r"^msgid \"\"\s*\nmsgstr \"\"\s*\n?", "", rest, flags=re.MULTILINE
|
||||
)
|
||||
sanitized = header + "\n\n" + rest_clean
|
||||
tmp = NamedTemporaryFile( # noqa: SIM115
|
||||
mode="w+", delete=False, suffix=".po", encoding="utf-8"
|
||||
|
|
@ -124,13 +130,19 @@ class Command(BaseCommand):
|
|||
for target_lang in target_langs:
|
||||
api_code = DEEPL_TARGET_LANGUAGES_MAPPING.get(target_lang)
|
||||
if not api_code:
|
||||
self.stdout.write(self.style.WARNING(f"Unknown language '{target_lang}'"))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f"Unknown language '{target_lang}'")
|
||||
)
|
||||
continue
|
||||
if api_code == "unsupported":
|
||||
self.stdout.write(self.style.WARNING(f"Unsupported language '{target_lang}'"))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f"Unsupported language '{target_lang}'")
|
||||
)
|
||||
continue
|
||||
|
||||
self.stdout.write(self.style.MIGRATE_HEADING(f"→ Translating into {target_lang}"))
|
||||
self.stdout.write(
|
||||
self.style.MIGRATE_HEADING(f"→ Translating into {target_lang}")
|
||||
)
|
||||
|
||||
configs = list(apps.get_app_configs()) + [RootDirectory()]
|
||||
|
||||
|
|
@ -138,9 +150,13 @@ class Command(BaseCommand):
|
|||
if app_conf.label not in target_apps:
|
||||
continue
|
||||
|
||||
en_path = os.path.join(app_conf.path, "locale", "en_GB", "LC_MESSAGES", "django.po")
|
||||
en_path = os.path.join(
|
||||
app_conf.path, "locale", "en_GB", "LC_MESSAGES", "django.po"
|
||||
)
|
||||
if not os.path.isfile(en_path):
|
||||
self.stdout.write(self.style.WARNING(f"• {app_conf.label}: no en_GB PO"))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f"• {app_conf.label}: no en_GB PO")
|
||||
)
|
||||
continue
|
||||
|
||||
self.stdout.write(f"• {app_conf.label}: loading English PO…")
|
||||
|
|
@ -148,9 +164,13 @@ class Command(BaseCommand):
|
|||
if not en_po:
|
||||
raise CommandError(f"Failed to load en_GB PO for {app_conf.label}")
|
||||
|
||||
missing = [e for e in en_po if e.msgid and not e.msgstr and not e.obsolete]
|
||||
missing = [
|
||||
e for e in en_po if e.msgid and not e.msgstr and not e.obsolete
|
||||
]
|
||||
if missing:
|
||||
self.stdout.write(self.style.NOTICE(f"⚠️ {len(missing)} missing in en_GB"))
|
||||
self.stdout.write(
|
||||
self.style.NOTICE(f"⚠️ {len(missing)} missing in en_GB")
|
||||
)
|
||||
for e in missing:
|
||||
default = e.msgid
|
||||
if readline:
|
||||
|
|
@ -193,7 +213,11 @@ class Command(BaseCommand):
|
|||
try:
|
||||
old_tgt = load_po_sanitized(str(tgt_path))
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.WARNING(f"Existing PO parse error({e!s}), starting fresh"))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f"Existing PO parse error({e!s}), starting fresh"
|
||||
)
|
||||
)
|
||||
|
||||
new_po = polib.POFile()
|
||||
new_po.metadata = en_po.metadata.copy()
|
||||
|
|
@ -215,7 +239,9 @@ class Command(BaseCommand):
|
|||
|
||||
to_trans = [e for e in new_po if not e.msgstr]
|
||||
if not to_trans:
|
||||
self.stdout.write(self.style.WARNING(f"All done for {app_conf.label}"))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f"All done for {app_conf.label}")
|
||||
)
|
||||
continue
|
||||
|
||||
protected = []
|
||||
|
|
@ -239,7 +265,9 @@ class Command(BaseCommand):
|
|||
|
||||
trans = result.get("translations", [])
|
||||
if len(trans) != len(to_trans):
|
||||
raise CommandError(f"Got {len(trans)} translations, expected {len(to_trans)}")
|
||||
raise CommandError(
|
||||
f"Got {len(trans)} translations, expected {len(to_trans)}"
|
||||
)
|
||||
|
||||
for entry, obj, pmap in zip(to_trans, trans, maps, strict=True):
|
||||
entry.msgstr = deplaceholderize(obj["text"], pmap)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ class Command(BaseCommand):
|
|||
def handle(self, *args: list[Any], **options: dict[Any, Any]) -> None:
|
||||
size: int = options["size"] # type: ignore [assignment]
|
||||
while True:
|
||||
batch_ids = list(Product.objects.filter(orderproduct__isnull=True).values_list("pk", flat=True)[:size])
|
||||
batch_ids = list(
|
||||
Product.objects.filter(orderproduct__isnull=True).values_list(
|
||||
"pk", flat=True
|
||||
)[:size]
|
||||
)
|
||||
if not batch_ids:
|
||||
break
|
||||
try:
|
||||
|
|
@ -29,7 +33,10 @@ class Command(BaseCommand):
|
|||
ProductImage.objects.filter(product_id__in=batch_ids).delete()
|
||||
Product.objects.filter(pk__in=batch_ids).delete()
|
||||
except Exception as e:
|
||||
self.stdout.write("Couldn't delete some of the products(will retry later): %s" % str(e))
|
||||
self.stdout.write(
|
||||
"Couldn't delete some of the products(will retry later): %s"
|
||||
% str(e)
|
||||
)
|
||||
continue
|
||||
self.stdout.write(f"Deleted {len(batch_ids)} products…")
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ class Command(BaseCommand):
|
|||
size: int = options["size"] # type: ignore [assignment]
|
||||
while True:
|
||||
batch_ids = list(
|
||||
Product.objects.filter(description__iexact="EVIBES_DELETED_PRODUCT").values_list("pk", flat=True)[:size]
|
||||
Product.objects.filter(
|
||||
description__iexact="EVIBES_DELETED_PRODUCT"
|
||||
).values_list("pk", flat=True)[:size]
|
||||
)
|
||||
if not batch_ids:
|
||||
break
|
||||
|
|
@ -31,7 +33,10 @@ class Command(BaseCommand):
|
|||
ProductImage.objects.filter(product_id__in=batch_ids).delete()
|
||||
Product.objects.filter(pk__in=batch_ids).delete()
|
||||
except Exception as e:
|
||||
self.stdout.write("Couldn't delete some of the products(will retry later): %s" % str(e))
|
||||
self.stdout.write(
|
||||
"Couldn't delete some of the products(will retry later): %s"
|
||||
% str(e)
|
||||
)
|
||||
continue
|
||||
self.stdout.write(f"Deleted {len(batch_ids)} products…")
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,14 @@ from engine.core.tasks import update_products_task
|
|||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args: list[Any], **options: dict[Any, Any]) -> None:
|
||||
self.stdout.write(self.style.SUCCESS("Starting fetching products task in worker container..."))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Starting fetching products task in worker container...")
|
||||
)
|
||||
|
||||
update_products_task.delay() # type: ignore [attr-defined]
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Started fetching products task in worker container without errors!"))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
"Started fetching products task in worker container without errors!"
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -53,19 +53,28 @@ class Command(BaseCommand):
|
|||
continue
|
||||
|
||||
if any(
|
||||
line.startswith("#~") or (line.startswith("#,") and line.lstrip("#, ").startswith("msgid "))
|
||||
line.startswith("#~")
|
||||
or (line.startswith("#,") and line.lstrip("#, ").startswith("msgid "))
|
||||
for line in ent
|
||||
):
|
||||
changed = True
|
||||
continue
|
||||
|
||||
fuzzy_idx = next(
|
||||
(i for i, line in enumerate(ent) if line.startswith("#,") and "fuzzy" in line),
|
||||
(
|
||||
i
|
||||
for i, line in enumerate(ent)
|
||||
if line.startswith("#,") and "fuzzy" in line
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if fuzzy_idx is not None:
|
||||
flags = [f.strip() for f in ent[fuzzy_idx][2:].split(",") if f.strip() != "fuzzy"]
|
||||
flags = [
|
||||
f.strip()
|
||||
for f in ent[fuzzy_idx][2:].split(",")
|
||||
if f.strip() != "fuzzy"
|
||||
]
|
||||
if flags:
|
||||
ent[fuzzy_idx] = "#, " + ", ".join(flags) + "\n"
|
||||
else:
|
||||
|
|
@ -73,7 +82,9 @@ class Command(BaseCommand):
|
|||
|
||||
ent = [line for line in ent if not line.startswith("#| msgid")]
|
||||
|
||||
ent = ['msgstr ""\n' if line.startswith("msgstr") else line for line in ent]
|
||||
ent = [
|
||||
'msgstr ""\n' if line.startswith("msgstr") else line for line in ent
|
||||
]
|
||||
|
||||
changed = True
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,13 @@ class Command(BaseCommand):
|
|||
for product in Product.objects.filter(stocks__isnull=False):
|
||||
for stock in product.stocks.all():
|
||||
try:
|
||||
stock.price = AbstractVendor.round_price_marketologically(stock.price)
|
||||
stock.price = AbstractVendor.round_price_marketologically(
|
||||
stock.price
|
||||
)
|
||||
stock.save()
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.WARNING(f"Couldn't fix price on {stock.uuid}"))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f"Couldn't fix price on {stock.uuid}")
|
||||
)
|
||||
self.stdout.write(self.style.WARNING(f"Error: {e}"))
|
||||
self.stdout.write(self.style.SUCCESS("Successfully fixed stocks' prices!"))
|
||||
|
|
|
|||
|
|
@ -151,43 +151,65 @@ class Command(BaseCommand):
|
|||
stored_date = datetime.min
|
||||
|
||||
if not (settings.RELEASE_DATE > stored_date):
|
||||
self.stdout.write(self.style.WARNING("Initialization skipped: already up-to-date."))
|
||||
self.stdout.write(
|
||||
self.style.WARNING("Initialization skipped: already up-to-date.")
|
||||
)
|
||||
return
|
||||
|
||||
Vendor.objects.get_or_create(name="INNER")
|
||||
|
||||
user_support, is_user_support_created = Group.objects.get_or_create(name="User Support")
|
||||
user_support, is_user_support_created = Group.objects.get_or_create(
|
||||
name="User Support"
|
||||
)
|
||||
if is_user_support_created:
|
||||
perms = Permission.objects.filter(codename__in=user_support_permissions)
|
||||
user_support.permissions.add(*perms)
|
||||
|
||||
stock_manager, is_stock_manager_created = Group.objects.get_or_create(name="Stock Manager")
|
||||
stock_manager, is_stock_manager_created = Group.objects.get_or_create(
|
||||
name="Stock Manager"
|
||||
)
|
||||
if is_stock_manager_created:
|
||||
perms = Permission.objects.filter(codename__in=stock_manager_permissions)
|
||||
stock_manager.permissions.add(*perms)
|
||||
|
||||
head_stock_manager, is_head_stock_manager_created = Group.objects.get_or_create(name="Head Stock Manager")
|
||||
head_stock_manager, is_head_stock_manager_created = Group.objects.get_or_create(
|
||||
name="Head Stock Manager"
|
||||
)
|
||||
if is_head_stock_manager_created:
|
||||
perms = Permission.objects.filter(codename__in=head_stock_manager_permissions)
|
||||
perms = Permission.objects.filter(
|
||||
codename__in=head_stock_manager_permissions
|
||||
)
|
||||
head_stock_manager.permissions.add(*perms)
|
||||
|
||||
marketing_admin, is_marketing_admin_created = Group.objects.get_or_create(name="Marketing Admin")
|
||||
marketing_admin, is_marketing_admin_created = Group.objects.get_or_create(
|
||||
name="Marketing Admin"
|
||||
)
|
||||
if is_marketing_admin_created:
|
||||
perms = Permission.objects.filter(codename__in=marketing_admin_permissions)
|
||||
marketing_admin.permissions.add(*perms)
|
||||
|
||||
e_commerce_admin, is_e_commerce_admin_created = Group.objects.get_or_create(name="E-Commerce Admin")
|
||||
e_commerce_admin, is_e_commerce_admin_created = Group.objects.get_or_create(
|
||||
name="E-Commerce Admin"
|
||||
)
|
||||
if is_e_commerce_admin_created:
|
||||
perms = Permission.objects.filter(codename__in=e_commerce_admin_permissions)
|
||||
e_commerce_admin.permissions.add(*perms)
|
||||
|
||||
valid_codes = [code for code, _ in settings.LANGUAGES]
|
||||
(User.objects.filter(Q(language="") | ~Q(language__in=valid_codes)).update(language=settings.LANGUAGE_CODE))
|
||||
(
|
||||
User.objects.filter(Q(language="") | ~Q(language__in=valid_codes)).update(
|
||||
language=settings.LANGUAGE_CODE
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
if not settings.DEBUG:
|
||||
initialized_path.write_text(settings.RELEASE_DATE.isoformat(), encoding="utf-8")
|
||||
initialized_path.write_text(
|
||||
settings.RELEASE_DATE.isoformat(), encoding="utf-8"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error("Failed to update .initialized file: %s", exc)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Successfully initialized must-have instances!"))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Successfully initialized must-have instances!")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,10 +14,17 @@ class Command(BaseCommand):
|
|||
|
||||
def reset_em(self, queryset: QuerySet[Any]) -> None:
|
||||
total = queryset.count()
|
||||
self.stdout.write(f"Starting slug rebuilding for {total} {queryset.model._meta.verbose_name_plural}")
|
||||
self.stdout.write(
|
||||
f"Starting slug rebuilding for {total} {queryset.model._meta.verbose_name_plural}"
|
||||
)
|
||||
for idx, instance in enumerate(queryset.iterator(), start=1):
|
||||
try:
|
||||
while queryset.filter(name=instance.name).exclude(uuid=instance.uuid).count() >= 1:
|
||||
while (
|
||||
queryset.filter(name=instance.name)
|
||||
.exclude(uuid=instance.uuid)
|
||||
.count()
|
||||
>= 1
|
||||
):
|
||||
instance.name = f"{instance.name} - {get_random_string(length=3, allowed_chars='0123456789')}"
|
||||
instance.save()
|
||||
instance.slug = None
|
||||
|
|
|
|||
|
|
@ -52,7 +52,9 @@ class Command(BaseCommand):
|
|||
module = importlib.import_module(module_path)
|
||||
model = getattr(module, model_name)
|
||||
except (ImportError, AttributeError) as e:
|
||||
raise CommandError(f"Could not import model '{model_name}' from '{module_path}': {e}") from e
|
||||
raise CommandError(
|
||||
f"Could not import model '{model_name}' from '{module_path}': {e}"
|
||||
) from e
|
||||
|
||||
dest_suffix = lang.replace("-", "_")
|
||||
dest_field = f"{field_name}_{dest_suffix}"
|
||||
|
|
@ -67,13 +69,17 @@ class Command(BaseCommand):
|
|||
if not auth_key:
|
||||
raise CommandError("Environment variable DEEPL_AUTH_KEY is not set.")
|
||||
|
||||
qs = model.objects.exclude(**{f"{field_name}__isnull": True}).exclude(**{f"{field_name}": ""})
|
||||
qs = model.objects.exclude(**{f"{field_name}__isnull": True}).exclude(
|
||||
**{f"{field_name}": ""}
|
||||
)
|
||||
total = qs.count()
|
||||
if total == 0:
|
||||
self.stdout.write("No instances with non-empty source field found.")
|
||||
return
|
||||
|
||||
self.stdout.write(f"Translating {total} objects from '{field_name}' into '{dest_field}'.")
|
||||
self.stdout.write(
|
||||
f"Translating {total} objects from '{field_name}' into '{dest_field}'."
|
||||
)
|
||||
|
||||
for obj in qs.iterator():
|
||||
src_text = getattr(obj, field_name)
|
||||
|
|
@ -92,7 +98,9 @@ class Command(BaseCommand):
|
|||
timeout=30,
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
self.stderr.write(f"DeepL API error for {obj.pk}: {resp.status_code} {resp.text}")
|
||||
self.stderr.write(
|
||||
f"DeepL API error for {obj.pk}: {resp.status_code} {resp.text}"
|
||||
)
|
||||
continue
|
||||
|
||||
data = resp.json()
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ class AddressManager(models.Manager):
|
|||
resp.raise_for_status()
|
||||
results = resp.json()
|
||||
if not results:
|
||||
raise ValueError(f"No geocoding result for address: {kwargs.get('raw_data')}")
|
||||
raise ValueError(
|
||||
f"No geocoding result for address: {kwargs.get('raw_data')}"
|
||||
)
|
||||
data = results[0]
|
||||
|
||||
addr = data.get("address", {})
|
||||
|
|
|
|||
|
|
@ -47,7 +47,9 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -111,7 +113,9 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -216,7 +220,9 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -607,7 +613,9 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -675,13 +683,17 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tag_name",
|
||||
models.CharField(
|
||||
help_text="internal tag identifier for the product tag", max_length=255, verbose_name="tag name"
|
||||
help_text="internal tag identifier for the product tag",
|
||||
max_length=255,
|
||||
verbose_name="tag name",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -893,7 +905,9 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -996,7 +1010,9 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -1409,13 +1425,17 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
"price",
|
||||
models.FloatField(
|
||||
default=0.0, help_text="final price to the customer after markups", verbose_name="selling price"
|
||||
default=0.0,
|
||||
help_text="final price to the customer after markups",
|
||||
verbose_name="selling price",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -1492,7 +1512,9 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -1516,7 +1538,14 @@ class Migration(migrations.Migration):
|
|||
verbose_name="vendor markup percentage",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(help_text="name of this vendor", max_length=255, verbose_name="vendor name")),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
help_text="name of this vendor",
|
||||
max_length=255,
|
||||
verbose_name="vendor name",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "vendor",
|
||||
|
|
@ -1556,7 +1585,9 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -1598,13 +1629,17 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
help_text="attribute group's name", max_length=255, verbose_name="attribute group's name"
|
||||
help_text="attribute group's name",
|
||||
max_length=255,
|
||||
verbose_name="attribute group's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -1820,7 +1855,9 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -1842,115 +1879,171 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_en_GB",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_ar_AR",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_cs_CZ",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_da_DK",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_de_DE",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_en_US",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_es_ES",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_fr_FR",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_hi_IN",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_it_IT",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_ja_JP",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_kk_KZ",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_nl_NL",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_pl_PL",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_pt_BR",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_ro_RO",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_ru_RU",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_zh_hans",
|
||||
models.CharField(
|
||||
help_text="name of this attribute", max_length=255, null=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -2002,119 +2095,160 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value",
|
||||
models.TextField(help_text="the specific value for this attribute", verbose_name="attribute value"),
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute",
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_en_GB",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_ar_AR",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_cs_CZ",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_da_DK",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_de_DE",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_en_US",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_es_ES",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_fr_FR",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_hi_IN",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_it_IT",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_ja_JP",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_kk_KZ",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_nl_NL",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_pl_PL",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_pt_BR",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_ro_RO",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_ru_RU",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_zh_hans",
|
||||
models.TextField(
|
||||
help_text="the specific value for this attribute", null=True, verbose_name="attribute value"
|
||||
help_text="the specific value for this attribute",
|
||||
null=True,
|
||||
verbose_name="attribute value",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -2166,7 +2300,9 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -2175,7 +2311,9 @@ class Migration(migrations.Migration):
|
|||
help_text="upload an image representing this category",
|
||||
null=True,
|
||||
upload_to="categories/",
|
||||
validators=[engine.core.validators.validate_category_image_dimensions],
|
||||
validators=[
|
||||
engine.core.validators.validate_category_image_dimensions
|
||||
],
|
||||
verbose_name="category image",
|
||||
),
|
||||
),
|
||||
|
|
@ -2194,7 +2332,9 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
help_text="provide a name for this category", max_length=255, verbose_name="category name"
|
||||
help_text="provide a name for this category",
|
||||
max_length=255,
|
||||
verbose_name="category name",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -2586,10 +2726,19 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
help_text="name of this brand",
|
||||
max_length=255,
|
||||
verbose_name="brand name",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(help_text="name of this brand", max_length=255, verbose_name="brand name")),
|
||||
(
|
||||
"category",
|
||||
models.ForeignKey(
|
||||
|
|
@ -2650,7 +2799,9 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
|
|||
|
|
@ -203,7 +203,8 @@ class Migration(migrations.Migration):
|
|||
migrations.AddIndex(
|
||||
model_name="orderproduct",
|
||||
index=django.contrib.postgres.indexes.GinIndex(
|
||||
fields=["notifications", "attributes"], name="core_orderp_notific_cd27e9_gin"
|
||||
fields=["notifications", "attributes"],
|
||||
name="core_orderp_notific_cd27e9_gin",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ class Migration(migrations.Migration):
|
|||
model_name="attribute",
|
||||
name="name",
|
||||
field=models.CharField(
|
||||
help_text="name of this attribute", max_length=255, unique=True, verbose_name="attribute's name"
|
||||
help_text="name of this attribute",
|
||||
max_length=255,
|
||||
unique=True,
|
||||
verbose_name="attribute's name",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
|
|
@ -216,7 +219,10 @@ class Migration(migrations.Migration):
|
|||
model_name="attributegroup",
|
||||
name="name",
|
||||
field=models.CharField(
|
||||
help_text="attribute group's name", max_length=255, unique=True, verbose_name="attribute group's name"
|
||||
help_text="attribute group's name",
|
||||
max_length=255,
|
||||
unique=True,
|
||||
verbose_name="attribute group's name",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
|
|
@ -421,14 +427,20 @@ class Migration(migrations.Migration):
|
|||
model_name="brand",
|
||||
name="name",
|
||||
field=models.CharField(
|
||||
help_text="name of this brand", max_length=255, unique=True, verbose_name="brand name"
|
||||
help_text="name of this brand",
|
||||
max_length=255,
|
||||
unique=True,
|
||||
verbose_name="brand name",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="category",
|
||||
name="name",
|
||||
field=models.CharField(
|
||||
help_text="provide a name for this category", max_length=255, unique=True, verbose_name="category name"
|
||||
help_text="provide a name for this category",
|
||||
max_length=255,
|
||||
unique=True,
|
||||
verbose_name="category name",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
|
|
@ -1049,7 +1061,10 @@ class Migration(migrations.Migration):
|
|||
model_name="vendor",
|
||||
name="name",
|
||||
field=models.CharField(
|
||||
help_text="name of this vendor", max_length=255, unique=True, verbose_name="vendor name"
|
||||
help_text="name of this vendor",
|
||||
max_length=255,
|
||||
unique=True,
|
||||
verbose_name="vendor name",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -44,14 +44,18 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
("num_downloads", models.IntegerField(default=0)),
|
||||
(
|
||||
"order_product",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="download", to="core.orderproduct"
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="download",
|
||||
to="core.orderproduct",
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -46,14 +46,23 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
"document",
|
||||
models.FileField(
|
||||
upload_to=engine.core.utils.get_product_uuid_as_path
|
||||
),
|
||||
),
|
||||
("document", models.FileField(upload_to=engine.core.utils.get_product_uuid_as_path)),
|
||||
(
|
||||
"product",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="documentaries", to="core.product"
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="documentaries",
|
||||
to="core.product",
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ def fix_duplicates(apps, schema_editor):
|
|||
if schema_editor:
|
||||
pass
|
||||
Order = apps.get_model("core", "Order")
|
||||
duplicates = Order.objects.values("human_readable_id").annotate(count=Count("uuid")).filter(count__gt=1)
|
||||
duplicates = (
|
||||
Order.objects.values("human_readable_id")
|
||||
.annotate(count=Count("uuid"))
|
||||
.filter(count__gt=1)
|
||||
)
|
||||
for duplicate in duplicates:
|
||||
h_id = duplicate["human_readable_id"]
|
||||
orders = Order.objects.filter(human_readable_id=h_id).order_by("uuid")
|
||||
|
|
|
|||
|
|
@ -47,15 +47,39 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
("street", models.CharField(max_length=255, null=True, verbose_name="street")),
|
||||
("district", models.CharField(max_length=255, null=True, verbose_name="district")),
|
||||
("city", models.CharField(max_length=100, null=True, verbose_name="city")),
|
||||
("region", models.CharField(max_length=100, null=True, verbose_name="region")),
|
||||
("postal_code", models.CharField(max_length=20, null=True, verbose_name="postal code")),
|
||||
("country", models.CharField(max_length=40, null=True, verbose_name="country")),
|
||||
(
|
||||
"street",
|
||||
models.CharField(max_length=255, null=True, verbose_name="street"),
|
||||
),
|
||||
(
|
||||
"district",
|
||||
models.CharField(
|
||||
max_length=255, null=True, verbose_name="district"
|
||||
),
|
||||
),
|
||||
(
|
||||
"city",
|
||||
models.CharField(max_length=100, null=True, verbose_name="city"),
|
||||
),
|
||||
(
|
||||
"region",
|
||||
models.CharField(max_length=100, null=True, verbose_name="region"),
|
||||
),
|
||||
(
|
||||
"postal_code",
|
||||
models.CharField(
|
||||
max_length=20, null=True, verbose_name="postal code"
|
||||
),
|
||||
),
|
||||
(
|
||||
"country",
|
||||
models.CharField(max_length=40, null=True, verbose_name="country"),
|
||||
),
|
||||
(
|
||||
"location",
|
||||
django.contrib.gis.db.models.fields.PointField(
|
||||
|
|
@ -69,26 +93,37 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"raw_data",
|
||||
models.JSONField(
|
||||
blank=True, help_text="full JSON response from geocoder for this address", null=True
|
||||
blank=True,
|
||||
help_text="full JSON response from geocoder for this address",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"api_response",
|
||||
models.JSONField(
|
||||
blank=True, help_text="stored JSON response from the geocoding service", null=True
|
||||
blank=True,
|
||||
help_text="stored JSON response from the geocoding service",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "address",
|
||||
"verbose_name_plural": "addresses",
|
||||
"indexes": [models.Index(fields=["location"], name="core_addres_locatio_eb6b39_idx")],
|
||||
"indexes": [
|
||||
models.Index(
|
||||
fields=["location"], name="core_addres_locatio_eb6b39_idx"
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -24,7 +24,12 @@ class Migration(migrations.Migration):
|
|||
model_name="category",
|
||||
name="slug",
|
||||
field=django_extensions.db.fields.AutoSlugField(
|
||||
allow_unicode=True, blank=True, editable=False, null=True, populate_from=("uuid", "name"), unique=True
|
||||
allow_unicode=True,
|
||||
blank=True,
|
||||
editable=False,
|
||||
null=True,
|
||||
populate_from=("uuid", "name"),
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(populate_slugs, reverse_code=migrations.RunPython.noop),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ class Migration(migrations.Migration):
|
|||
model_name="address",
|
||||
name="address_line",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="address line for the customer", null=True, verbose_name="address line"
|
||||
blank=True,
|
||||
help_text="address line for the customer",
|
||||
null=True,
|
||||
verbose_name="address line",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -44,13 +44,17 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tag_name",
|
||||
models.CharField(
|
||||
help_text="internal tag identifier for the product tag", max_length=255, verbose_name="tag name"
|
||||
help_text="internal tag identifier for the product tag",
|
||||
max_length=255,
|
||||
verbose_name="tag name",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -247,7 +251,10 @@ class Migration(migrations.Migration):
|
|||
"verbose_name": "category tag",
|
||||
"verbose_name_plural": "category tags",
|
||||
},
|
||||
bases=(django_prometheus.models.ExportModelOperationsMixin("category_tag"), models.Model),
|
||||
bases=(
|
||||
django_prometheus.models.ExportModelOperationsMixin("category_tag"),
|
||||
models.Model,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="category",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import engine.core.utils.db
|
||||
import django_extensions.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
import engine.core.utils.db
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import engine.core.utils
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import engine.core.utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
|
|
@ -23,6 +24,8 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name="vendor",
|
||||
name="users",
|
||||
field=models.ManyToManyField(blank=True, related_name="vendors", to=settings.AUTH_USER_MODEL),
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name="vendors", to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -21,14 +21,18 @@ def backfill_sku(apps, schema_editor):
|
|||
|
||||
while True:
|
||||
ids = list(
|
||||
Product.objects.filter(sku__isnull=True, pk__gt=last_pk).order_by("pk").values_list("pk", flat=True)[:BATCH]
|
||||
Product.objects.filter(sku__isnull=True, pk__gt=last_pk)
|
||||
.order_by("pk")
|
||||
.values_list("pk", flat=True)[:BATCH]
|
||||
)
|
||||
if not ids:
|
||||
break
|
||||
|
||||
updates = []
|
||||
for pk in ids:
|
||||
updates.append(Product(pk=pk, sku=generate_unique_sku(make_candidate, taken)))
|
||||
updates.append(
|
||||
Product(pk=pk, sku=generate_unique_sku(make_candidate, taken))
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
Product.objects.bulk_update(updates, ["sku"], batch_size=BATCH)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import engine.core.utils
|
||||
from django.db import migrations, models
|
||||
|
||||
import engine.core.utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
import django_extensions.db.fields
|
||||
import django_prometheus.models
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
|
@ -55,11 +56,15 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
(
|
||||
"integration_url",
|
||||
models.URLField(blank=True, help_text="URL of the integration", null=True),
|
||||
models.URLField(
|
||||
blank=True, help_text="URL of the integration", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"authentication",
|
||||
models.JSONField(blank=True, help_text="authentication credentials", null=True),
|
||||
models.JSONField(
|
||||
blank=True, help_text="authentication credentials", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"attributes",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import engine.core.utils
|
||||
from django.db import migrations, models
|
||||
|
||||
import engine.core.utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
|
|
|
|||
|
|
@ -329,19 +329,27 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
migrations.AddIndex(
|
||||
model_name="order",
|
||||
index=models.Index(fields=["user", "status"], name="core_order_user_id_4407f8_idx"),
|
||||
index=models.Index(
|
||||
fields=["user", "status"], name="core_order_user_id_4407f8_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="order",
|
||||
index=models.Index(fields=["status", "buy_time"], name="core_order_status_4a088a_idx"),
|
||||
index=models.Index(
|
||||
fields=["status", "buy_time"], name="core_order_status_4a088a_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="orderproduct",
|
||||
index=models.Index(fields=["order", "status"], name="core_orderp_order_i_d16192_idx"),
|
||||
index=models.Index(
|
||||
fields=["order", "status"], name="core_orderp_order_i_d16192_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="orderproduct",
|
||||
index=models.Index(fields=["product", "status"], name="core_orderp_product_ee8abb_idx"),
|
||||
index=models.Index(
|
||||
fields=["product", "status"], name="core_orderp_product_ee8abb_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="product",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name="stock",
|
||||
name="system_attributes",
|
||||
field=models.JSONField(blank=True, default=dict, verbose_name="system attributes"),
|
||||
field=models.JSONField(
|
||||
blank=True, default=dict, verbose_name="system attributes"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -121,7 +121,9 @@ class Vendor(ExportModelOperationsMixin("vendor"), NiceModel): # type: ignore [
|
|||
authentication = JSONField(
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_("stores credentials and endpoints required for vendor communication"),
|
||||
help_text=_(
|
||||
"stores credentials and endpoints required for vendor communication"
|
||||
),
|
||||
verbose_name=_("authentication info"),
|
||||
)
|
||||
markup_percent = IntegerField(
|
||||
|
|
@ -138,8 +140,12 @@ class Vendor(ExportModelOperationsMixin("vendor"), NiceModel): # type: ignore [
|
|||
null=False,
|
||||
unique=True,
|
||||
)
|
||||
users = ManyToManyField(to=settings.AUTH_USER_MODEL, related_name="vendors", blank=True)
|
||||
b2b_auth_token = CharField(default=generate_human_readable_token, max_length=20, null=True, blank=True)
|
||||
users = ManyToManyField(
|
||||
to=settings.AUTH_USER_MODEL, related_name="vendors", blank=True
|
||||
)
|
||||
b2b_auth_token = CharField(
|
||||
default=generate_human_readable_token, max_length=20, null=True, blank=True
|
||||
)
|
||||
last_processing_response = FileField(
|
||||
upload_to=get_vendor_name_as_path,
|
||||
blank=True,
|
||||
|
|
@ -339,10 +345,15 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel): #
|
|||
def get_tree_depth(self):
|
||||
if self.is_leaf_node():
|
||||
return 0
|
||||
return self.get_descendants().aggregate(max_depth=Max("level"))["max_depth"] - self.get_level()
|
||||
return (
|
||||
self.get_descendants().aggregate(max_depth=Max("level"))["max_depth"]
|
||||
- self.get_level()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def bulk_prefetch_filterable_attributes(cls, categories: Iterable["Category"]) -> None:
|
||||
def bulk_prefetch_filterable_attributes(
|
||||
cls, categories: Iterable["Category"]
|
||||
) -> None:
|
||||
cat_list = [c for c in categories]
|
||||
if not cat_list:
|
||||
return
|
||||
|
|
@ -383,7 +394,10 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel): #
|
|||
"value_type": value_type,
|
||||
}
|
||||
cat_bucket[attr_id] = bucket
|
||||
if len(bucket["possible_values"]) < 128 and value not in bucket["possible_values"]:
|
||||
if (
|
||||
len(bucket["possible_values"]) < 128
|
||||
and value not in bucket["possible_values"]
|
||||
):
|
||||
bucket["possible_values"].append(value)
|
||||
|
||||
for c in cat_list:
|
||||
|
|
@ -401,7 +415,9 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel): #
|
|||
attribute__is_filterable=True,
|
||||
value_length__lte=30,
|
||||
)
|
||||
.values_list("attribute_id", "attribute__name", "attribute__value_type", "value")
|
||||
.values_list(
|
||||
"attribute_id", "attribute__name", "attribute__value_type", "value"
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
|
|
@ -415,7 +431,10 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel): #
|
|||
"value_type": value_type,
|
||||
}
|
||||
by_attr[attr_id] = bucket
|
||||
if len(bucket["possible_values"]) < 128 and value not in bucket["possible_values"]:
|
||||
if (
|
||||
len(bucket["possible_values"]) < 128
|
||||
and value not in bucket["possible_values"]
|
||||
):
|
||||
bucket["possible_values"].append(value)
|
||||
|
||||
return list(by_attr.values()) # type: ignore [arg-type]
|
||||
|
|
@ -557,7 +576,9 @@ class Stock(ExportModelOperationsMixin("stock"), NiceModel): # type: ignore [mi
|
|||
verbose_name=_("digital file"),
|
||||
upload_to="downloadables/",
|
||||
)
|
||||
system_attributes = JSONField(default=dict, verbose_name=_("system attributes"), blank=True)
|
||||
system_attributes = JSONField(
|
||||
default=dict, verbose_name=_("system attributes"), blank=True
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.vendor.name} - {self.product!s}"
|
||||
|
|
@ -756,7 +777,9 @@ class Attribute(ExportModelOperationsMixin("attribute"), NiceModel): # type: ig
|
|||
is_filterable = BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("is filterable"),
|
||||
help_text=_("designates whether this attribute can be used for filtering or not"),
|
||||
help_text=_(
|
||||
"designates whether this attribute can be used for filtering or not"
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -991,7 +1014,9 @@ class Documentary(ExportModelOperationsMixin("attribute_group"), NiceModel): #
|
|||
|
||||
is_publicly_visible = True
|
||||
|
||||
product = ForeignKey(to="core.Product", on_delete=CASCADE, related_name="documentaries")
|
||||
product = ForeignKey(
|
||||
to="core.Product", on_delete=CASCADE, related_name="documentaries"
|
||||
)
|
||||
document = FileField(upload_to=get_product_uuid_as_path)
|
||||
|
||||
class Meta:
|
||||
|
|
@ -1044,7 +1069,11 @@ class Address(ExportModelOperationsMixin("address"), NiceModel): # type: ignore
|
|||
help_text=_("geolocation point: (longitude, latitude)"),
|
||||
)
|
||||
|
||||
raw_data = JSONField(blank=True, null=True, help_text=_("full JSON response from geocoder for this address"))
|
||||
raw_data = JSONField(
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_("full JSON response from geocoder for this address"),
|
||||
)
|
||||
|
||||
api_response = JSONField(
|
||||
blank=True,
|
||||
|
|
@ -1052,7 +1081,9 @@ class Address(ExportModelOperationsMixin("address"), NiceModel): # type: ignore
|
|||
help_text=_("stored JSON response from the geocoding service"),
|
||||
)
|
||||
|
||||
user = ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=CASCADE, blank=True, null=True)
|
||||
user = ForeignKey(
|
||||
to=settings.AUTH_USER_MODEL, on_delete=CASCADE, blank=True, null=True
|
||||
)
|
||||
|
||||
objects = AddressManager()
|
||||
|
||||
|
|
@ -1147,7 +1178,9 @@ class PromoCode(ExportModelOperationsMixin("promocode"), NiceModel): # type: ig
|
|||
self.discount_amount is None and self.discount_percent is None
|
||||
):
|
||||
raise ValidationError(
|
||||
_("only one type of discount should be defined (amount or percent), but not both or neither.")
|
||||
_(
|
||||
"only one type of discount should be defined (amount or percent), but not both or neither."
|
||||
)
|
||||
)
|
||||
return super().save(
|
||||
force_insert=force_insert,
|
||||
|
|
@ -1176,12 +1209,18 @@ class PromoCode(ExportModelOperationsMixin("promocode"), NiceModel): # type: ig
|
|||
promo_amount = order.total_price
|
||||
|
||||
if self.discount_type == "percent":
|
||||
promo_amount -= round(promo_amount * (float(self.discount_percent) / 100), 2) # type: ignore [arg-type]
|
||||
order.attributes.update({"promocode_uuid": str(self.uuid), "final_price": promo_amount})
|
||||
promo_amount -= round(
|
||||
promo_amount * (float(self.discount_percent) / 100), 2
|
||||
) # type: ignore [arg-type]
|
||||
order.attributes.update(
|
||||
{"promocode_uuid": str(self.uuid), "final_price": promo_amount}
|
||||
)
|
||||
order.save()
|
||||
elif self.discount_type == "amount":
|
||||
promo_amount -= round(float(self.discount_amount), 2) # type: ignore [arg-type]
|
||||
order.attributes.update({"promocode_uuid": str(self.uuid), "final_price": promo_amount})
|
||||
order.attributes.update(
|
||||
{"promocode_uuid": str(self.uuid), "final_price": promo_amount}
|
||||
)
|
||||
order.save()
|
||||
else:
|
||||
raise ValueError(_(f"invalid discount type for promocode {self.uuid}"))
|
||||
|
|
@ -1296,8 +1335,13 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
self.user.save()
|
||||
return False
|
||||
with suppress(Exception):
|
||||
return (self.attributes.get("is_business", False) if self.attributes else False) or (
|
||||
(self.user.attributes.get("is_business", False) and self.user.attributes.get("business_identificator")) # type: ignore [union-attr]
|
||||
return (
|
||||
self.attributes.get("is_business", False) if self.attributes else False
|
||||
) or (
|
||||
(
|
||||
self.user.attributes.get("is_business", False)
|
||||
and self.user.attributes.get("business_identificator")
|
||||
) # type: ignore [union-attr]
|
||||
if self.user
|
||||
else False
|
||||
)
|
||||
|
|
@ -1348,7 +1392,9 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
attributes = []
|
||||
|
||||
if self.status not in ["PENDING", "MOMENTAL"]:
|
||||
raise ValueError(_("you cannot add products to an order that is not a pending one"))
|
||||
raise ValueError(
|
||||
_("you cannot add products to an order that is not a pending one")
|
||||
)
|
||||
try:
|
||||
product = Product.objects.get(uuid=product_uuid)
|
||||
|
||||
|
|
@ -1357,10 +1403,14 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
|
||||
buy_price = product.price
|
||||
|
||||
promotions = Promotion.objects.filter(is_active=True, products__in=[product]).order_by("discount_percent")
|
||||
promotions = Promotion.objects.filter(
|
||||
is_active=True, products__in=[product]
|
||||
).order_by("discount_percent")
|
||||
|
||||
if promotions.exists():
|
||||
buy_price -= round(product.price * (promotions.first().discount_percent / 100), 2) # type: ignore [union-attr]
|
||||
buy_price -= round(
|
||||
product.price * (promotions.first().discount_percent / 100), 2
|
||||
) # type: ignore [union-attr]
|
||||
|
||||
order_product, is_created = OrderProduct.objects.get_or_create(
|
||||
product=product,
|
||||
|
|
@ -1370,7 +1420,9 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
)
|
||||
if not is_created and update_quantity:
|
||||
if product.quantity < order_product.quantity + 1:
|
||||
raise BadRequest(_("you cannot add more products than available in stock"))
|
||||
raise BadRequest(
|
||||
_("you cannot add more products than available in stock")
|
||||
)
|
||||
order_product.quantity += 1
|
||||
order_product.buy_price = product.price
|
||||
order_product.save()
|
||||
|
|
@ -1392,7 +1444,9 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
attributes = {}
|
||||
|
||||
if self.status not in ["PENDING", "MOMENTAL"]:
|
||||
raise ValueError(_("you cannot remove products from an order that is not a pending one"))
|
||||
raise ValueError(
|
||||
_("you cannot remove products from an order that is not a pending one")
|
||||
)
|
||||
try:
|
||||
product = Product.objects.get(uuid=product_uuid)
|
||||
order_product = self.order_products.get(product=product, order=self)
|
||||
|
|
@ -1412,12 +1466,16 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
raise Http404(_(f"{name} does not exist: {uuid}")) from dne
|
||||
except OrderProduct.DoesNotExist as dne:
|
||||
name = "OrderProduct"
|
||||
query = f"product: {product_uuid}, order: {self.uuid}, attributes: {attributes}"
|
||||
query = (
|
||||
f"product: {product_uuid}, order: {self.uuid}, attributes: {attributes}"
|
||||
)
|
||||
raise Http404(_(f"{name} does not exist with query <{query}>")) from dne
|
||||
|
||||
def remove_all_products(self):
|
||||
if self.status not in ["PENDING", "MOMENTAL"]:
|
||||
raise ValueError(_("you cannot remove products from an order that is not a pending one"))
|
||||
raise ValueError(
|
||||
_("you cannot remove products from an order that is not a pending one")
|
||||
)
|
||||
for order_product in self.order_products.all():
|
||||
self.order_products.remove(order_product)
|
||||
order_product.delete()
|
||||
|
|
@ -1425,7 +1483,9 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
|
||||
def remove_products_of_a_kind(self, product_uuid):
|
||||
if self.status not in ["PENDING", "MOMENTAL"]:
|
||||
raise ValueError(_("you cannot remove products from an order that is not a pending one"))
|
||||
raise ValueError(
|
||||
_("you cannot remove products from an order that is not a pending one")
|
||||
)
|
||||
try:
|
||||
product = Product.objects.get(uuid=product_uuid)
|
||||
order_product = self.order_products.get(product=product, order=self)
|
||||
|
|
@ -1439,7 +1499,10 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
|
||||
@property
|
||||
def is_whole_digital(self):
|
||||
return self.order_products.count() == self.order_products.filter(product__is_digital=True).count()
|
||||
return (
|
||||
self.order_products.count()
|
||||
== self.order_products.filter(product__is_digital=True).count()
|
||||
)
|
||||
|
||||
def apply_promocode(self, promocode_uuid):
|
||||
try:
|
||||
|
|
@ -1448,10 +1511,21 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
raise Http404(_("promocode does not exist")) from dne
|
||||
return promocode.use(self)
|
||||
|
||||
def apply_addresses(self, billing_address_uuid: str | None = None, shipping_address_uuid: str | None = None):
|
||||
def apply_addresses(
|
||||
self,
|
||||
billing_address_uuid: str | None = None,
|
||||
shipping_address_uuid: str | None = None,
|
||||
):
|
||||
try:
|
||||
if not any([shipping_address_uuid, billing_address_uuid]) and not self.is_whole_digital:
|
||||
raise ValueError(_("you can only buy physical products with shipping address specified"))
|
||||
if (
|
||||
not any([shipping_address_uuid, billing_address_uuid])
|
||||
and not self.is_whole_digital
|
||||
):
|
||||
raise ValueError(
|
||||
_(
|
||||
"you can only buy physical products with shipping address specified"
|
||||
)
|
||||
)
|
||||
|
||||
if billing_address_uuid and not shipping_address_uuid:
|
||||
shipping_address = Address.objects.get(uuid=billing_address_uuid)
|
||||
|
|
@ -1491,9 +1565,13 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
order.bulk_add_products(chosen_products, update_quantity=True)
|
||||
|
||||
if config.DISABLED_COMMERCE:
|
||||
raise DisabledCommerceError(_("you can not buy at this moment, please try again in a few minutes"))
|
||||
raise DisabledCommerceError(
|
||||
_("you can not buy at this moment, please try again in a few minutes")
|
||||
)
|
||||
|
||||
if (not force_balance and not force_payment) or (force_balance and force_payment):
|
||||
if (not force_balance and not force_payment) or (
|
||||
force_balance and force_payment
|
||||
):
|
||||
raise ValueError(_("invalid force value"))
|
||||
|
||||
if any([billing_address, shipping_address]):
|
||||
|
|
@ -1512,7 +1590,9 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
|
||||
amount = self.attributes.get("final_amount") or order.total_price
|
||||
|
||||
if self.attributes.get("promocode_uuid") and not self.attributes.get("final_amount"):
|
||||
if self.attributes.get("promocode_uuid") and not self.attributes.get(
|
||||
"final_amount"
|
||||
):
|
||||
amount = order.apply_promocode(self.attributes.get("promocode_uuid"))
|
||||
|
||||
if promocode_uuid and not self.attributes.get("final_amount"):
|
||||
|
|
@ -1522,9 +1602,9 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
raise ValueError(_("you cannot buy an order without a user"))
|
||||
|
||||
if type(order.user.attributes) is dict:
|
||||
if order.user.attributes.get("is_business", False) or order.user.attributes.get(
|
||||
"business_identificator", ""
|
||||
):
|
||||
if order.user.attributes.get(
|
||||
"is_business", False
|
||||
) or order.user.attributes.get("business_identificator", ""):
|
||||
if type(order.attributes) is not dict:
|
||||
order.attributes = {}
|
||||
order.attributes.update({"is_business": True})
|
||||
|
|
@ -1538,7 +1618,9 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
match force:
|
||||
case "balance":
|
||||
if order.user.payments_balance.amount < amount:
|
||||
raise NotEnoughMoneyError(_("insufficient funds to complete the order"))
|
||||
raise NotEnoughMoneyError(
|
||||
_("insufficient funds to complete the order")
|
||||
)
|
||||
with transaction.atomic():
|
||||
order.status = "CREATED"
|
||||
order.buy_time = timezone.now()
|
||||
|
|
@ -1558,14 +1640,20 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
case _:
|
||||
raise ValueError(_("invalid force value"))
|
||||
|
||||
def buy_without_registration(self, products: list, promocode_uuid, **kwargs) -> Transaction | None:
|
||||
def buy_without_registration(
|
||||
self, products: list, promocode_uuid, **kwargs
|
||||
) -> Transaction | None:
|
||||
if config.DISABLED_COMMERCE:
|
||||
raise DisabledCommerceError(_("you can not buy at this moment, please try again in a few minutes"))
|
||||
raise DisabledCommerceError(
|
||||
_("you can not buy at this moment, please try again in a few minutes")
|
||||
)
|
||||
|
||||
if len(products) < 1:
|
||||
raise ValueError(_("you cannot purchase an empty order!"))
|
||||
|
||||
customer_name = kwargs.get("customer_name") or kwargs.get("business_identificator")
|
||||
customer_name = kwargs.get("customer_name") or kwargs.get(
|
||||
"business_identificator"
|
||||
)
|
||||
customer_email = kwargs.get("customer_email")
|
||||
customer_phone_number = kwargs.get("customer_phone_number")
|
||||
|
||||
|
|
@ -1581,17 +1669,25 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
available_payment_methods = cache.get("payment_methods").get("payment_methods")
|
||||
|
||||
if payment_method not in available_payment_methods:
|
||||
raise ValueError(_(f"invalid payment method: {payment_method} from {available_payment_methods}"))
|
||||
raise ValueError(
|
||||
_(
|
||||
f"invalid payment method: {payment_method} from {available_payment_methods}"
|
||||
)
|
||||
)
|
||||
|
||||
billing_customer_address_uuid = kwargs.get("billing_customer_address")
|
||||
shipping_customer_address_uuid = kwargs.get("shipping_customer_address")
|
||||
|
||||
self.apply_addresses(billing_customer_address_uuid, shipping_customer_address_uuid)
|
||||
self.apply_addresses(
|
||||
billing_customer_address_uuid, shipping_customer_address_uuid
|
||||
)
|
||||
|
||||
for product_uuid in products:
|
||||
self.add_product(product_uuid)
|
||||
|
||||
amount = self.apply_promocode(promocode_uuid) if promocode_uuid else self.total_price
|
||||
amount = (
|
||||
self.apply_promocode(promocode_uuid) if promocode_uuid else self.total_price
|
||||
)
|
||||
|
||||
self.status = "CREATED"
|
||||
|
||||
|
|
@ -1635,7 +1731,9 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
def update_order_products_statuses(self, status: str = "PENDING"):
|
||||
self.order_products.update(status=status)
|
||||
|
||||
def bulk_add_products(self, products: list[dict[str, Any]], update_quantity: bool = False):
|
||||
def bulk_add_products(
|
||||
self, products: list[dict[str, Any]], update_quantity: bool = False
|
||||
):
|
||||
for product in products:
|
||||
self.add_product(
|
||||
product.get("uuid") or product.get("product_uuid"),
|
||||
|
|
@ -1657,7 +1755,9 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
crm_links = OrderCrmLink.objects.filter(order=self)
|
||||
if crm_links.exists():
|
||||
crm_link = crm_links.first()
|
||||
crm_integration = create_object(crm_link.crm.integration_location, crm_link.crm.name)
|
||||
crm_integration = create_object(
|
||||
crm_link.crm.integration_location, crm_link.crm.name
|
||||
)
|
||||
try:
|
||||
crm_integration.process_order_changes(self)
|
||||
return True
|
||||
|
|
@ -1678,19 +1778,25 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
|||
return True
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"failed to trigger CRM integration %s for order %s: %s", crm.name, self.uuid, str(e), exc_info=True
|
||||
"failed to trigger CRM integration %s for order %s: %s",
|
||||
crm.name,
|
||||
self.uuid,
|
||||
str(e),
|
||||
exc_info=True,
|
||||
)
|
||||
return False
|
||||
|
||||
@property
|
||||
def business_identificator(self) -> str | None:
|
||||
if self.attributes:
|
||||
return self.attributes.get("business_identificator") or self.attributes.get("businessIdentificator")
|
||||
return self.attributes.get("business_identificator") or self.attributes.get(
|
||||
"businessIdentificator"
|
||||
)
|
||||
if self.user:
|
||||
if self.user.attributes:
|
||||
return self.user.attributes.get("business_identificator") or self.user.attributes.get(
|
||||
"businessIdentificator"
|
||||
)
|
||||
return self.user.attributes.get(
|
||||
"business_identificator"
|
||||
) or self.user.attributes.get("businessIdentificator")
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -1716,7 +1822,9 @@ class Feedback(ExportModelOperationsMixin("feedback"), NiceModel): # type: igno
|
|||
on_delete=CASCADE,
|
||||
blank=False,
|
||||
null=False,
|
||||
help_text=_("references the specific product in an order that this feedback is about"),
|
||||
help_text=_(
|
||||
"references the specific product in an order that this feedback is about"
|
||||
),
|
||||
verbose_name=_("related order product"),
|
||||
)
|
||||
rating = FloatField(
|
||||
|
|
@ -1728,7 +1836,11 @@ class Feedback(ExportModelOperationsMixin("feedback"), NiceModel): # type: igno
|
|||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.order_product and self.order_product.order and self.order_product.order.user:
|
||||
if (
|
||||
self.order_product
|
||||
and self.order_product.order
|
||||
and self.order_product.order.user
|
||||
):
|
||||
return f"{self.rating} by {self.order_product.order.user.email}"
|
||||
return f"{self.rating} | {self.uuid}"
|
||||
|
||||
|
|
@ -1838,7 +1950,9 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): # t
|
|||
"errors": [
|
||||
{
|
||||
"detail": (
|
||||
error if error else f"Something went wrong with {self.uuid} for some reason..."
|
||||
error
|
||||
if error
|
||||
else f"Something went wrong with {self.uuid} for some reason..."
|
||||
)
|
||||
},
|
||||
]
|
||||
|
|
@ -1883,17 +1997,27 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): # t
|
|||
if action == "add":
|
||||
if not feedback_exists:
|
||||
if self.order.status == "FINISHED":
|
||||
return Feedback.objects.create(rating=rating, comment=comment, order_product=self)
|
||||
return Feedback.objects.create(
|
||||
rating=rating, comment=comment, order_product=self
|
||||
)
|
||||
else:
|
||||
raise ValueError(_("you cannot feedback an order which is not received"))
|
||||
raise ValueError(
|
||||
_("you cannot feedback an order which is not received")
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class CustomerRelationshipManagementProvider(ExportModelOperationsMixin("crm_provider"), NiceModel): # type: ignore [misc]
|
||||
class CustomerRelationshipManagementProvider(
|
||||
ExportModelOperationsMixin("crm_provider"), NiceModel
|
||||
): # type: ignore [misc]
|
||||
name = CharField(max_length=128, unique=True, verbose_name=_("name"))
|
||||
integration_url = URLField(blank=True, null=True, help_text=_("URL of the integration"))
|
||||
authentication = JSONField(blank=True, null=True, help_text=_("authentication credentials"))
|
||||
integration_url = URLField(
|
||||
blank=True, null=True, help_text=_("URL of the integration")
|
||||
)
|
||||
authentication = JSONField(
|
||||
blank=True, null=True, help_text=_("authentication credentials")
|
||||
)
|
||||
attributes = JSONField(blank=True, null=True, verbose_name=_("attributes"))
|
||||
integration_location = CharField(max_length=128, blank=True, null=True)
|
||||
default = BooleanField(default=False)
|
||||
|
|
@ -1931,7 +2055,11 @@ class CustomerRelationshipManagementProvider(ExportModelOperationsMixin("crm_pro
|
|||
|
||||
class OrderCrmLink(ExportModelOperationsMixin("order_crm_link"), NiceModel): # type: ignore
|
||||
order = ForeignKey(to=Order, on_delete=PROTECT, related_name="crm_links")
|
||||
crm = ForeignKey(to=CustomerRelationshipManagementProvider, on_delete=PROTECT, related_name="order_links")
|
||||
crm = ForeignKey(
|
||||
to=CustomerRelationshipManagementProvider,
|
||||
on_delete=PROTECT,
|
||||
related_name="order_links",
|
||||
)
|
||||
crm_lead_id = CharField(max_length=30, unique=True, db_index=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
|
@ -1954,7 +2082,9 @@ class DigitalAssetDownload(ExportModelOperationsMixin("attribute_group"), NiceMo
|
|||
|
||||
is_publicly_visible = False
|
||||
|
||||
order_product = OneToOneField(to=OrderProduct, on_delete=CASCADE, related_name="download")
|
||||
order_product = OneToOneField(
|
||||
to=OrderProduct, on_delete=CASCADE, related_name="download"
|
||||
)
|
||||
num_downloads = IntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
|
|
@ -1966,6 +2096,4 @@ class DigitalAssetDownload(ExportModelOperationsMixin("attribute_group"), NiceMo
|
|||
|
||||
@property
|
||||
def url(self):
|
||||
return (
|
||||
f"https://api.{settings.BASE_DOMAIN}/download/{urlsafe_base64_encode(force_bytes(self.order_product.uuid))}"
|
||||
)
|
||||
return f"https://api.{settings.BASE_DOMAIN}/download/{urlsafe_base64_encode(force_bytes(self.order_product.uuid))}"
|
||||
|
|
|
|||
|
|
@ -60,10 +60,15 @@ class EvibesPermission(permissions.BasePermission):
|
|||
return True
|
||||
|
||||
perm_prefix = self.ACTION_PERM_MAP.get(action)
|
||||
if perm_prefix and request.user.has_perm(f"{app_label}.{perm_prefix}_{model_name}"):
|
||||
if perm_prefix and request.user.has_perm(
|
||||
f"{app_label}.{perm_prefix}_{model_name}"
|
||||
):
|
||||
return True
|
||||
|
||||
return bool(action in ("list", "retrieve") and getattr(model, "is_publicly_visible", False))
|
||||
return bool(
|
||||
action in ("list", "retrieve")
|
||||
and getattr(model, "is_publicly_visible", False)
|
||||
)
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
|
|
@ -76,7 +81,10 @@ class EvibesPermission(permissions.BasePermission):
|
|||
model_name = obj._meta.model_name
|
||||
action = view.action
|
||||
perm_prefix = self.ACTION_PERM_MAP.get(action)
|
||||
return bool(perm_prefix and request.user.has_perm(f"{app_label}.{perm_prefix}_{model_name}"))
|
||||
return bool(
|
||||
perm_prefix
|
||||
and request.user.has_perm(f"{app_label}.{perm_prefix}_{model_name}")
|
||||
)
|
||||
|
||||
perm_prefix = self.ACTION_PERM_MAP.get(view.action)
|
||||
return bool(
|
||||
|
|
@ -100,7 +108,9 @@ class EvibesPermission(permissions.BasePermission):
|
|||
return queryset.none()
|
||||
|
||||
base = queryset.filter(is_active=True, user=request.user)
|
||||
if request.user.has_perm(f"{app_label}.{self.ACTION_PERM_MAP.get(view.action)}_{model_name}"):
|
||||
if request.user.has_perm(
|
||||
f"{app_label}.{self.ACTION_PERM_MAP.get(view.action)}_{model_name}"
|
||||
):
|
||||
return queryset.filter(is_active=True)
|
||||
return base
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,10 @@ from engine.core.models import (
|
|||
Vendor,
|
||||
Wishlist,
|
||||
)
|
||||
from engine.core.serializers.simple import CategorySimpleSerializer, ProductSimpleSerializer
|
||||
from engine.core.serializers.simple import (
|
||||
CategorySimpleSerializer,
|
||||
ProductSimpleSerializer,
|
||||
)
|
||||
from engine.core.serializers.utility import AddressSerializer
|
||||
from engine.core.typing import FilterableAttribute
|
||||
|
||||
|
|
@ -85,7 +88,11 @@ class CategoryDetailSerializer(ModelSerializer):
|
|||
else:
|
||||
children = obj.children.filter(is_active=True)
|
||||
|
||||
return CategorySimpleSerializer(children, many=True, context=self.context).data if obj.children.exists() else [] # type: ignore [return-value]
|
||||
return (
|
||||
CategorySimpleSerializer(children, many=True, context=self.context).data
|
||||
if obj.children.exists()
|
||||
else []
|
||||
) # type: ignore [return-value]
|
||||
|
||||
|
||||
class BrandDetailSerializer(ModelSerializer):
|
||||
|
|
|
|||
|
|
@ -59,7 +59,11 @@ class CategorySimpleSerializer(ModelSerializer): # type: ignore [type-arg]
|
|||
else:
|
||||
children = obj.children.filter(is_active=True)
|
||||
|
||||
return CategorySimpleSerializer(children, many=True, context=self.context).data if obj.children.exists() else [] # type: ignore [return-value]
|
||||
return (
|
||||
CategorySimpleSerializer(children, many=True, context=self.context).data
|
||||
if obj.children.exists()
|
||||
else []
|
||||
) # type: ignore [return-value]
|
||||
|
||||
|
||||
class BrandSimpleSerializer(ModelSerializer): # type: ignore [type-arg]
|
||||
|
|
|
|||
|
|
@ -86,7 +86,11 @@ class DoFeedbackSerializer(Serializer): # type: ignore [type-arg]
|
|||
|
||||
def validate(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
if data["action"] == "add" and not all([data["comment"], data["rating"]]):
|
||||
raise ValidationError(_("you must provide a comment, rating, and order product uuid to add feedback."))
|
||||
raise ValidationError(
|
||||
_(
|
||||
"you must provide a comment, rating, and order product uuid to add feedback."
|
||||
)
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
|
|
@ -150,11 +154,15 @@ class RemoveWishlistProductSerializer(Serializer): # type: ignore [type-arg]
|
|||
|
||||
|
||||
class BulkAddWishlistProductSerializer(Serializer): # type: ignore [type-arg]
|
||||
product_uuids = ListField(child=CharField(required=True), allow_empty=False, max_length=64)
|
||||
product_uuids = ListField(
|
||||
child=CharField(required=True), allow_empty=False, max_length=64
|
||||
)
|
||||
|
||||
|
||||
class BulkRemoveWishlistProductSerializer(Serializer): # type: ignore [type-arg]
|
||||
product_uuids = ListField(child=CharField(required=True), allow_empty=False, max_length=64)
|
||||
product_uuids = ListField(
|
||||
child=CharField(required=True), allow_empty=False, max_length=64
|
||||
)
|
||||
|
||||
|
||||
class BuyOrderSerializer(Serializer): # type: ignore [type-arg]
|
||||
|
|
|
|||
|
|
@ -13,21 +13,34 @@ from sentry_sdk import capture_exception
|
|||
|
||||
from engine.core.crm import any_crm_integrations
|
||||
from engine.core.crm.exceptions import CRMException
|
||||
from engine.core.models import Category, DigitalAssetDownload, Order, Product, PromoCode, Wishlist
|
||||
from engine.core.models import (
|
||||
Category,
|
||||
DigitalAssetDownload,
|
||||
Order,
|
||||
Product,
|
||||
PromoCode,
|
||||
Wishlist,
|
||||
)
|
||||
from engine.core.utils import (
|
||||
generate_human_readable_id,
|
||||
resolve_translations_for_elasticsearch,
|
||||
)
|
||||
from engine.core.utils.emailing import send_order_created_email, send_order_finished_email, send_promocode_created_email
|
||||
from evibes.utils.misc import create_object
|
||||
from engine.core.utils.emailing import (
|
||||
send_order_created_email,
|
||||
send_order_finished_email,
|
||||
send_promocode_created_email,
|
||||
)
|
||||
from engine.vibes_auth.models import User
|
||||
from evibes.utils.misc import create_object
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@receiver(post_save, sender=User)
|
||||
def create_order_on_user_creation_signal(instance: User, created: bool, **kwargs: dict[Any, Any]) -> None:
|
||||
def create_order_on_user_creation_signal(
|
||||
instance: User, created: bool, **kwargs: dict[Any, Any]
|
||||
) -> None:
|
||||
if created:
|
||||
try:
|
||||
Order.objects.create(user=instance, status="PENDING")
|
||||
|
|
@ -37,27 +50,35 @@ def create_order_on_user_creation_signal(instance: User, created: bool, **kwargs
|
|||
if Order.objects.filter(human_readable_id=human_readable_id).exists():
|
||||
human_readable_id = generate_human_readable_id()
|
||||
continue
|
||||
Order.objects.create(user=instance, status="PENDING", human_readable_id=human_readable_id)
|
||||
Order.objects.create(
|
||||
user=instance, status="PENDING", human_readable_id=human_readable_id
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@receiver(post_save, sender=User)
|
||||
def create_wishlist_on_user_creation_signal(instance: User, created: bool, **kwargs: dict[Any, Any]) -> None:
|
||||
def create_wishlist_on_user_creation_signal(
|
||||
instance: User, created: bool, **kwargs: dict[Any, Any]
|
||||
) -> None:
|
||||
if created:
|
||||
Wishlist.objects.create(user=instance)
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@receiver(post_save, sender=User)
|
||||
def create_promocode_on_user_referring(instance: User, created: bool, **kwargs: dict[Any, Any]) -> None:
|
||||
def create_promocode_on_user_referring(
|
||||
instance: User, created: bool, **kwargs: dict[Any, Any]
|
||||
) -> None:
|
||||
try:
|
||||
if type(instance.attributes) is not dict:
|
||||
instance.attributes = {}
|
||||
instance.save()
|
||||
|
||||
if created and instance.attributes.get("referrer", ""):
|
||||
referrer_uuid = urlsafe_base64_decode(instance.attributes.get("referrer", "")).decode()
|
||||
referrer_uuid = urlsafe_base64_decode(
|
||||
instance.attributes.get("referrer", "")
|
||||
).decode()
|
||||
referrer = User.objects.get(uuid=referrer_uuid)
|
||||
code = f"WELCOME-{get_random_string(6)}"
|
||||
PromoCode.objects.create(
|
||||
|
|
@ -93,7 +114,9 @@ def process_order_changes(instance: Order, created: bool, **kwargs: dict[Any, An
|
|||
human_readable_id = generate_human_readable_id()
|
||||
while True:
|
||||
try:
|
||||
if Order.objects.filter(human_readable_id=human_readable_id).exists():
|
||||
if Order.objects.filter(
|
||||
human_readable_id=human_readable_id
|
||||
).exists():
|
||||
human_readable_id = generate_human_readable_id()
|
||||
continue
|
||||
Order.objects.create(
|
||||
|
|
@ -109,11 +132,15 @@ def process_order_changes(instance: Order, created: bool, **kwargs: dict[Any, An
|
|||
if not instance.is_whole_digital:
|
||||
send_order_created_email.delay(str(instance.uuid))
|
||||
|
||||
for order_product in instance.order_products.filter(status="DELIVERING", product__is_digital=True):
|
||||
for order_product in instance.order_products.filter(
|
||||
status="DELIVERING", product__is_digital=True
|
||||
):
|
||||
if not order_product.product:
|
||||
continue
|
||||
|
||||
stocks_qs = order_product.product.stocks.filter(digital_asset__isnull=False).exclude(digital_asset="")
|
||||
stocks_qs = order_product.product.stocks.filter(
|
||||
digital_asset__isnull=False
|
||||
).exclude(digital_asset="")
|
||||
|
||||
stock = stocks_qs.first()
|
||||
|
||||
|
|
@ -124,8 +151,12 @@ def process_order_changes(instance: Order, created: bool, **kwargs: dict[Any, An
|
|||
|
||||
if has_file:
|
||||
order_product.status = "FINISHED"
|
||||
DigitalAssetDownload.objects.get_or_create(order_product=order_product)
|
||||
order_product.order.user.payments_balance.amount -= order_product.buy_price # type: ignore [union-attr, operator]
|
||||
DigitalAssetDownload.objects.get_or_create(
|
||||
order_product=order_product
|
||||
)
|
||||
order_product.order.user.payments_balance.amount -= (
|
||||
order_product.buy_price
|
||||
) # type: ignore [union-attr, operator]
|
||||
order_product.order.user.payments_balance.save() # type: ignore [union-attr]
|
||||
order_product.save()
|
||||
continue
|
||||
|
|
@ -134,24 +165,37 @@ def process_order_changes(instance: Order, created: bool, **kwargs: dict[Any, An
|
|||
|
||||
try:
|
||||
vendor_name = (
|
||||
order_product.product.stocks.filter(price=order_product.buy_price).first().vendor.name.lower() # type: ignore [union-attr, attr-defined, misc]
|
||||
order_product.product.stocks.filter(
|
||||
price=order_product.buy_price
|
||||
)
|
||||
.first()
|
||||
.vendor.name.lower() # type: ignore [union-attr, attr-defined, misc]
|
||||
)
|
||||
|
||||
vendor = create_object(f"core.vendors.{vendor_name}", f"{vendor_name.title()}Vendor")
|
||||
vendor = create_object(
|
||||
f"core.vendors.{vendor_name}", f"{vendor_name.title()}Vendor"
|
||||
)
|
||||
|
||||
vendor.buy_order_product(order_product) # type: ignore [attr-defined]
|
||||
|
||||
except Exception as e:
|
||||
order_product.add_error(f"Failed to buy {order_product.uuid}. Reason: {e}...")
|
||||
order_product.add_error(
|
||||
f"Failed to buy {order_product.uuid}. Reason: {e}..."
|
||||
)
|
||||
|
||||
else:
|
||||
instance.finalize()
|
||||
|
||||
if instance.order_products.filter(status="FAILED").count() == instance.order_products.count():
|
||||
if (
|
||||
instance.order_products.filter(status="FAILED").count()
|
||||
== instance.order_products.count()
|
||||
):
|
||||
instance.status = "FAILED"
|
||||
instance.save()
|
||||
|
||||
if instance.status == "FINISHED" and not instance.attributes.get("system_email_sent", False):
|
||||
if instance.status == "FINISHED" and not instance.attributes.get(
|
||||
"system_email_sent", False
|
||||
):
|
||||
instance.attributes["system_email_sent"] = True
|
||||
instance.save()
|
||||
send_order_finished_email.delay(str(instance.uuid))
|
||||
|
|
@ -159,7 +203,9 @@ def process_order_changes(instance: Order, created: bool, **kwargs: dict[Any, An
|
|||
|
||||
# noinspection PyUnusedLocal
|
||||
@receiver(post_save, sender=Product)
|
||||
def update_product_name_lang(instance: Product, created: bool, **kwargs: dict[Any, Any]) -> None:
|
||||
def update_product_name_lang(
|
||||
instance: Product, created: bool, **kwargs: dict[Any, Any]
|
||||
) -> None:
|
||||
if created:
|
||||
pass
|
||||
resolve_translations_for_elasticsearch(instance, "name")
|
||||
|
|
@ -168,7 +214,9 @@ def update_product_name_lang(instance: Product, created: bool, **kwargs: dict[An
|
|||
|
||||
# noinspection PyUnusedLocal
|
||||
@receiver(post_save, sender=Category)
|
||||
def update_category_name_lang(instance: Category, created: bool, **kwargs: dict[Any, Any]) -> None:
|
||||
def update_category_name_lang(
|
||||
instance: Category, created: bool, **kwargs: dict[Any, Any]
|
||||
) -> None:
|
||||
if created:
|
||||
pass
|
||||
resolve_translations_for_elasticsearch(instance, "name")
|
||||
|
|
@ -177,6 +225,8 @@ def update_category_name_lang(instance: Category, created: bool, **kwargs: dict[
|
|||
|
||||
# noinspection PyUnusedLocal
|
||||
@receiver(post_save, sender=PromoCode)
|
||||
def send_promocode_creation_email(instance: PromoCode, created: bool, **kwargs: dict[Any, Any]) -> None:
|
||||
def send_promocode_creation_email(
|
||||
instance: PromoCode, created: bool, **kwargs: dict[Any, Any]
|
||||
) -> None:
|
||||
if created:
|
||||
send_promocode_created_email.delay(str(instance.uuid))
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ class StaticPagesSitemap(SitemapLanguageMixin, Sitemap): # type: ignore [type-a
|
|||
},
|
||||
]
|
||||
|
||||
for static_post_page in Post.objects.filter(is_static_page=True, is_active=True).only(
|
||||
"title", "slug", "modified"
|
||||
):
|
||||
for static_post_page in Post.objects.filter(
|
||||
is_static_page=True, is_active=True
|
||||
).only("title", "slug", "modified"):
|
||||
pages.append(
|
||||
{
|
||||
"name": static_post_page.title,
|
||||
|
|
|
|||
|
|
@ -134,7 +134,9 @@ def remove_stale_product_images() -> tuple[bool, str]:
|
|||
# Load all current product UUIDs into a set.
|
||||
# This query returns all product UUIDs (as strings or UUID objects).
|
||||
current_product_uuids = set(Product.objects.values_list("uuid", flat=True))
|
||||
logger.info("Loaded %d product UUIDs from the database.", len(current_product_uuids))
|
||||
logger.info(
|
||||
"Loaded %d product UUIDs from the database.", len(current_product_uuids)
|
||||
)
|
||||
|
||||
# Iterate through all subdirectories in the products folder.
|
||||
for entry in os.listdir(products_dir):
|
||||
|
|
@ -151,7 +153,9 @@ def remove_stale_product_images() -> tuple[bool, str]:
|
|||
if product_uuid not in current_product_uuids:
|
||||
try:
|
||||
shutil.rmtree(entry_path)
|
||||
logger.info("Removed stale product images directory: %s", entry_path)
|
||||
logger.info(
|
||||
"Removed stale product images directory: %s", entry_path
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Error removing directory %s: %s", entry_path, e)
|
||||
return True, "Successfully removed stale product images."
|
||||
|
|
@ -192,7 +196,9 @@ def process_promotions() -> tuple[bool, str]:
|
|||
)
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
logger.warning("Couldn't fetch holiday data for %s: %s", checked_date, str(e))
|
||||
logger.warning(
|
||||
"Couldn't fetch holiday data for %s: %s", checked_date, str(e)
|
||||
)
|
||||
return False, f"Couldn't fetch holiday data for {checked_date}: {e!s}"
|
||||
holidays = response.json()
|
||||
if holidays:
|
||||
|
|
@ -224,7 +230,8 @@ def process_promotions() -> tuple[bool, str]:
|
|||
selected_products.append(product)
|
||||
|
||||
promotion = Promotion.objects.update_or_create(
|
||||
name=promotion_name, defaults={"discount_percent": discount_percent, "is_active": True}
|
||||
name=promotion_name,
|
||||
defaults={"discount_percent": discount_percent, "is_active": True},
|
||||
)[0]
|
||||
|
||||
for product in selected_products:
|
||||
|
|
|
|||
|
|
@ -20,12 +20,20 @@ class DRFCoreViewsTests(TestCase):
|
|||
)
|
||||
self.user_password = "Str0ngPass!word2"
|
||||
self.user = User.objects.create(
|
||||
email="test-superuser@email.com", password=self.user_password, is_active=True, is_verified=True
|
||||
email="test-superuser@email.com",
|
||||
password=self.user_password,
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
)
|
||||
|
||||
def _get_authorization_token(self, user):
|
||||
serializer = TokenObtainPairSerializer(
|
||||
data={"email": user.email, "password": self.superuser_password if user.is_superuser else self.user_password}
|
||||
data={
|
||||
"email": user.email,
|
||||
"password": self.superuser_password
|
||||
if user.is_superuser
|
||||
else self.user_password,
|
||||
}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return serializer.validated_data["access_token"]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
from django.urls import include, path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from engine.core.sitemaps import BrandSitemap, CategorySitemap, ProductSitemap, StaticPagesSitemap
|
||||
from engine.core.sitemaps import (
|
||||
BrandSitemap,
|
||||
CategorySitemap,
|
||||
ProductSitemap,
|
||||
StaticPagesSitemap,
|
||||
)
|
||||
from engine.core.views import (
|
||||
CacheOperatorView,
|
||||
ContactUsView,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ from django.utils.translation import get_language
|
|||
from graphene import Context
|
||||
from rest_framework.request import Request
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -183,7 +182,11 @@ def generate_human_readable_id(length: int = 6) -> str:
|
|||
"""
|
||||
chars = [secrets.choice(CROCKFORD) for _ in range(length)]
|
||||
|
||||
pos = (secrets.randbelow(length - 1) + 1) if secrets.choice([True, False]) else (length // 2)
|
||||
pos = (
|
||||
(secrets.randbelow(length - 1) + 1)
|
||||
if secrets.choice([True, False])
|
||||
else (length // 2)
|
||||
)
|
||||
|
||||
chars.insert(pos, "-")
|
||||
return "".join(chars)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ def get_cached_value(user: Type[User], key: str, default: Any = None) -> Any:
|
|||
return None
|
||||
|
||||
|
||||
def set_cached_value(user: Type[User], key: str, value: object, timeout: int = 3600) -> None | object:
|
||||
def set_cached_value(
|
||||
user: Type[User], key: str, value: object, timeout: int = 3600
|
||||
) -> None | object:
|
||||
if user.is_staff or user.is_superuser:
|
||||
cache.set(key, value, timeout)
|
||||
return value
|
||||
|
|
@ -37,13 +39,17 @@ def set_cached_value(user: Type[User], key: str, value: object, timeout: int = 3
|
|||
return None
|
||||
|
||||
|
||||
def web_cache(request: Request | Context, key: str, data: dict[str, Any], timeout: int) -> dict[str, Any]:
|
||||
def web_cache(
|
||||
request: Request | Context, key: str, data: dict[str, Any], timeout: int
|
||||
) -> dict[str, Any]:
|
||||
if not data and not timeout:
|
||||
return {"data": get_cached_value(request.user, key)} # type: ignore [assignment, arg-type]
|
||||
if (data and not timeout) or (timeout and not data):
|
||||
raise BadRequest(_("both data and timeout are required"))
|
||||
if not 0 < int(timeout) < 216000:
|
||||
raise BadRequest(_("invalid timeout value, it must be between 0 and 216000 seconds"))
|
||||
raise BadRequest(
|
||||
_("invalid timeout value, it must be between 0 and 216000 seconds")
|
||||
)
|
||||
return {"data": set_cached_value(request.user, key, data, timeout)} # type: ignore [assignment, arg-type]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -18,14 +18,19 @@ def get_period_order_products(
|
|||
statuses = ["FINISHED"]
|
||||
current = now()
|
||||
perioded = current - period
|
||||
orders = Order.objects.filter(status="FINISHED", buy_time__lte=current, buy_time__gte=perioded)
|
||||
orders = Order.objects.filter(
|
||||
status="FINISHED", buy_time__lte=current, buy_time__gte=perioded
|
||||
)
|
||||
return OrderProduct.objects.filter(status__in=statuses, order__in=orders)
|
||||
|
||||
|
||||
def get_revenue(clear: bool = True, period: timedelta = timedelta(days=30)) -> float:
|
||||
order_products = get_period_order_products(period)
|
||||
total: float = (
|
||||
order_products.aggregate(total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0)).get("total") or 0.0
|
||||
order_products.aggregate(
|
||||
total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0)
|
||||
).get("total")
|
||||
or 0.0
|
||||
)
|
||||
try:
|
||||
total = float(total)
|
||||
|
|
@ -62,7 +67,10 @@ def get_revenue(clear: bool = True, period: timedelta = timedelta(days=30)) -> f
|
|||
def get_returns(period: timedelta = timedelta(days=30)) -> float:
|
||||
order_products = get_period_order_products(period, ["RETURNED"])
|
||||
total_returns: float = (
|
||||
order_products.aggregate(total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0)).get("total") or 0.0
|
||||
order_products.aggregate(
|
||||
total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0)
|
||||
).get("total")
|
||||
or 0.0
|
||||
)
|
||||
try:
|
||||
return round(float(total_returns), 2)
|
||||
|
|
@ -74,11 +82,15 @@ def get_total_processed_orders(period: timedelta = timedelta(days=30)) -> int:
|
|||
return get_period_order_products(period, ["RETURNED", "FINISHED"]).count()
|
||||
|
||||
|
||||
def get_daily_finished_orders_count(period: timedelta = timedelta(days=30)) -> dict[date, int]:
|
||||
def get_daily_finished_orders_count(
|
||||
period: timedelta = timedelta(days=30),
|
||||
) -> dict[date, int]:
|
||||
current = now()
|
||||
period_start = current - period
|
||||
qs = (
|
||||
Order.objects.filter(status="FINISHED", buy_time__lte=current, buy_time__gte=period_start)
|
||||
Order.objects.filter(
|
||||
status="FINISHED", buy_time__lte=current, buy_time__gte=period_start
|
||||
)
|
||||
.annotate(day=TruncDate("buy_time"))
|
||||
.values("day")
|
||||
.annotate(cnt=Count("pk"))
|
||||
|
|
@ -93,7 +105,9 @@ def get_daily_finished_orders_count(period: timedelta = timedelta(days=30)) -> d
|
|||
return result
|
||||
|
||||
|
||||
def get_daily_gross_revenue(period: timedelta = timedelta(days=30)) -> dict[date, float]:
|
||||
def get_daily_gross_revenue(
|
||||
period: timedelta = timedelta(days=30),
|
||||
) -> dict[date, float]:
|
||||
qs = (
|
||||
get_period_order_products(period, ["FINISHED"]) # OrderProduct queryset
|
||||
.annotate(day=TruncDate("order__buy_time"))
|
||||
|
|
@ -114,7 +128,9 @@ def get_daily_gross_revenue(period: timedelta = timedelta(days=30)) -> dict[date
|
|||
return result
|
||||
|
||||
|
||||
def get_top_returned_products(period: timedelta = timedelta(days=30), limit: int = 10) -> list[dict[str, Any]]:
|
||||
def get_top_returned_products(
|
||||
period: timedelta = timedelta(days=30), limit: int = 10
|
||||
) -> list[dict[str, Any]]:
|
||||
current = now()
|
||||
period_start = current - period
|
||||
qs = (
|
||||
|
|
@ -161,7 +177,12 @@ def get_customer_mix(period: timedelta = timedelta(days=30)) -> dict[str, int]:
|
|||
current = now()
|
||||
period_start = current - period
|
||||
period_users = (
|
||||
Order.objects.filter(status="FINISHED", buy_time__lte=current, buy_time__gte=period_start, user__isnull=False)
|
||||
Order.objects.filter(
|
||||
status="FINISHED",
|
||||
buy_time__lte=current,
|
||||
buy_time__gte=period_start,
|
||||
user__isnull=False,
|
||||
)
|
||||
.values_list("user_id", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
|
|
@ -169,7 +190,9 @@ def get_customer_mix(period: timedelta = timedelta(days=30)) -> dict[str, int]:
|
|||
return {"new": 0, "returning": 0}
|
||||
|
||||
lifetime_counts = (
|
||||
Order.objects.filter(status="FINISHED", user_id__in=period_users).values("user_id").annotate(c=Count("pk"))
|
||||
Order.objects.filter(status="FINISHED", user_id__in=period_users)
|
||||
.values("user_id")
|
||||
.annotate(c=Count("pk"))
|
||||
)
|
||||
new_cnt = 0
|
||||
ret_cnt = 0
|
||||
|
|
@ -182,7 +205,9 @@ def get_customer_mix(period: timedelta = timedelta(days=30)) -> dict[str, int]:
|
|||
return {"new": new_cnt, "returning": ret_cnt}
|
||||
|
||||
|
||||
def get_top_categories_by_qty(period: timedelta = timedelta(days=30), limit: int = 10) -> list[dict[str, Any]]:
|
||||
def get_top_categories_by_qty(
|
||||
period: timedelta = timedelta(days=30), limit: int = 10
|
||||
) -> list[dict[str, Any]]:
|
||||
current = now()
|
||||
period_start = current - period
|
||||
qs = (
|
||||
|
|
@ -222,7 +247,9 @@ def get_top_categories_by_qty(period: timedelta = timedelta(days=30), limit: int
|
|||
return result
|
||||
|
||||
|
||||
def get_shipped_vs_digital_mix(period: timedelta = timedelta(days=30)) -> dict[str, float | int]:
|
||||
def get_shipped_vs_digital_mix(
|
||||
period: timedelta = timedelta(days=30),
|
||||
) -> dict[str, float | int]:
|
||||
current = now()
|
||||
period_start = current - period
|
||||
qs = (
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ def unicode_slugify_function(content: Any) -> str:
|
|||
|
||||
|
||||
class TweakedAutoSlugField(AutoSlugField):
|
||||
def get_slug_fields(self, model_instance: Model, lookup_value: str | Callable[[Any], Any]) -> str | Model:
|
||||
def get_slug_fields(
|
||||
self, model_instance: Model, lookup_value: str | Callable[[Any], Any]
|
||||
) -> str | Model:
|
||||
if callable(lookup_value):
|
||||
return f"{lookup_value(model_instance)}"
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ def contact_us_email(contact_info) -> tuple[bool, str]:
|
|||
"email": contact_info.get("email"),
|
||||
"name": contact_info.get("name"),
|
||||
"subject": contact_info.get("subject", "Without subject"),
|
||||
"phone_number": contact_info.get("phone_number", "Without phone number"),
|
||||
"phone_number": contact_info.get(
|
||||
"phone_number", "Without phone number"
|
||||
),
|
||||
"message": contact_info.get("message"),
|
||||
"config": config,
|
||||
},
|
||||
|
|
@ -56,7 +58,13 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]:
|
|||
if type(order.attributes) is not dict:
|
||||
order.attributes = {}
|
||||
|
||||
if not any([order.user, order.attributes.get("email", None), order.attributes.get("customer_email", None)]):
|
||||
if not any(
|
||||
[
|
||||
order.user,
|
||||
order.attributes.get("email", None),
|
||||
order.attributes.get("customer_email", None),
|
||||
]
|
||||
):
|
||||
return False, f"Order's user not found with the given pk: {order_pk}"
|
||||
|
||||
language = settings.LANGUAGE_CODE
|
||||
|
|
@ -72,7 +80,9 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]:
|
|||
email = EmailMessage(
|
||||
_(f"{settings.PROJECT_NAME} | order confirmation"),
|
||||
render_to_string(
|
||||
"digital_order_created_email.html" if order.is_whole_digital else "shipped_order_created_email.html",
|
||||
"digital_order_created_email.html"
|
||||
if order.is_whole_digital
|
||||
else "shipped_order_created_email.html",
|
||||
{
|
||||
"order": order,
|
||||
"today": datetime.today(),
|
||||
|
|
@ -122,7 +132,12 @@ def send_order_finished_email(order_pk: str) -> tuple[bool, str]:
|
|||
)
|
||||
email.content_subtype = "html"
|
||||
result = email.send()
|
||||
logger.debug("Order %s: Tried to send email to %s, resulted with %s", order.pk, order.user.email, result)
|
||||
logger.debug(
|
||||
"Order %s: Tried to send email to %s, resulted with %s",
|
||||
order.pk,
|
||||
order.user.email,
|
||||
result,
|
||||
)
|
||||
|
||||
def send_thank_you_email(ops: list[OrderProduct]) -> None:
|
||||
if ops:
|
||||
|
|
@ -204,7 +219,10 @@ def send_promocode_created_email(promocode_pk: str) -> tuple[bool, str]:
|
|||
email.content_subtype = "html"
|
||||
result = email.send()
|
||||
logger.debug(
|
||||
"Promocode %s: Tried to send email to %s, resulted with %s", promocode.pk, promocode.user.email, result
|
||||
"Promocode %s: Tried to send email to %s, resulted with %s",
|
||||
promocode.pk,
|
||||
promocode.user.email,
|
||||
result,
|
||||
)
|
||||
|
||||
return True, str(promocode.uuid)
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ def breadcrumb_schema(items):
|
|||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{"@type": "ListItem", "position": i + 1, "name": name, "item": url} for i, (name, url) in enumerate(items)
|
||||
{"@type": "ListItem", "position": i + 1, "name": name, "item": url}
|
||||
for i, (name, url) in enumerate(items)
|
||||
],
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +43,10 @@ def item_list_schema(urls):
|
|||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ItemList",
|
||||
"itemListElement": [{"@type": "ListItem", "position": i + 1, "url": u} for i, u in enumerate(urls)],
|
||||
"itemListElement": [
|
||||
{"@type": "ListItem", "position": i + 1, "url": u}
|
||||
for i, u in enumerate(urls)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -54,7 +58,9 @@ def product_schema(product, images, rating=None):
|
|||
"@type": "Offer",
|
||||
"price": round(stock.price, 2),
|
||||
"priceCurrency": settings.CURRENCY_CODE,
|
||||
"availability": "https://schema.org/InStock" if stock.quantity > 0 else "https://schema.org/OutOfStock",
|
||||
"availability": "https://schema.org/InStock"
|
||||
if stock.quantity > 0
|
||||
else "https://schema.org/OutOfStock",
|
||||
"sku": stock.sku,
|
||||
"url": f"https://{settings.BASE_DOMAIN}/product/{product.slug}",
|
||||
}
|
||||
|
|
@ -65,7 +71,9 @@ def product_schema(product, images, rating=None):
|
|||
"name": product.name,
|
||||
"description": product.description or "",
|
||||
"sku": product.partnumber or "",
|
||||
"brand": {"@type": "Brand", "name": product.brand.name} if product.brand else None,
|
||||
"brand": {"@type": "Brand", "name": product.brand.name}
|
||||
if product.brand
|
||||
else None,
|
||||
"image": [img.image.url for img in images] or [],
|
||||
"offers": offers[:1] if offers else None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ def get_vendors_integrations(name: str | None = None) -> list[AbstractVendor]:
|
|||
vendors_integrations.append(create_object(module_name, class_name))
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Couldn't load integration %s for vendor %s: %s", vendor.integration_path, vendor.name, str(e)
|
||||
"Couldn't load integration %s for vendor %s: %s",
|
||||
vendor.integration_path,
|
||||
vendor.name,
|
||||
str(e),
|
||||
)
|
||||
|
||||
return vendors_integrations
|
||||
|
|
|
|||
|
|
@ -16,4 +16,8 @@ def validate_category_image_dimensions(
|
|||
return
|
||||
|
||||
if int(width) > max_width or int(height) > max_height: # type: ignore [arg-type]
|
||||
raise ValidationError(_(f"image dimensions should not exceed w{max_width} x h{max_height} pixels"))
|
||||
raise ValidationError(
|
||||
_(
|
||||
f"image dimensions should not exceed w{max_width} x h{max_height} pixels"
|
||||
)
|
||||
)
|
||||
|
|
|
|||
99
engine/core/vendors/__init__.py
vendored
99
engine/core/vendors/__init__.py
vendored
|
|
@ -28,9 +28,9 @@ from engine.core.models import (
|
|||
Stock,
|
||||
Vendor,
|
||||
)
|
||||
from evibes.utils.misc import LoggingError, LogLevel
|
||||
from engine.payments.errors import RatesError
|
||||
from engine.payments.utils import get_rates
|
||||
from evibes.utils.misc import LoggingError, LogLevel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -145,7 +145,9 @@ class AbstractVendor:
|
|||
|
||||
if len(json_bytes) > size_threshold:
|
||||
buffer = BytesIO()
|
||||
with gzip.GzipFile(fileobj=buffer, mode="wb", compresslevel=9) as gz_file:
|
||||
with gzip.GzipFile(
|
||||
fileobj=buffer, mode="wb", compresslevel=9
|
||||
) as gz_file:
|
||||
gz_file.write(json_bytes)
|
||||
|
||||
compressed_data = buffer.getvalue()
|
||||
|
|
@ -157,15 +159,22 @@ class AbstractVendor:
|
|||
|
||||
self.log(LogLevel.DEBUG, f"Saving vendor's response to {filename}")
|
||||
|
||||
vendor_instance.last_processing_response.save(filename, content, save=True)
|
||||
vendor_instance.last_processing_response.save(
|
||||
filename, content, save=True
|
||||
)
|
||||
|
||||
self.log(LogLevel.DEBUG, f"Saved vendor's response to {filename} successfuly!")
|
||||
self.log(
|
||||
LogLevel.DEBUG,
|
||||
f"Saved vendor's response to {filename} successfuly!",
|
||||
)
|
||||
|
||||
return
|
||||
raise VendorDebuggingError("Could not save response")
|
||||
|
||||
@staticmethod
|
||||
def chunk_data(data: list[Any] | None = None, num_chunks: int = 20) -> list[list[Any]] | list[Any]:
|
||||
def chunk_data(
|
||||
data: list[Any] | None = None, num_chunks: int = 20
|
||||
) -> list[list[Any]] | list[Any]:
|
||||
if not data:
|
||||
return []
|
||||
total = len(data)
|
||||
|
|
@ -234,19 +243,25 @@ class AbstractVendor:
|
|||
return value, "string"
|
||||
|
||||
@staticmethod
|
||||
def auto_resolver_helper(model: type[Brand] | type[Category], resolving_name: str) -> Brand | Category | None:
|
||||
def auto_resolver_helper(
|
||||
model: type[Brand] | type[Category], resolving_name: str
|
||||
) -> Brand | Category | None:
|
||||
queryset = model.objects.filter(name=resolving_name)
|
||||
if not queryset.exists():
|
||||
if len(resolving_name) > 255:
|
||||
resolving_name = resolving_name[:255]
|
||||
return model.objects.get_or_create(name=resolving_name, defaults={"is_active": False})[0]
|
||||
return model.objects.get_or_create(
|
||||
name=resolving_name, defaults={"is_active": False}
|
||||
)[0]
|
||||
elif queryset.filter(is_active=True).count() > 1:
|
||||
queryset = queryset.filter(is_active=True)
|
||||
elif queryset.filter(is_active=False).count() > 1:
|
||||
queryset = queryset.filter(is_active=False)
|
||||
chosen = queryset.first()
|
||||
if not chosen:
|
||||
raise VendorError(f"No matching {model.__name__} found with name {resolving_name!r}...")
|
||||
raise VendorError(
|
||||
f"No matching {model.__name__} found with name {resolving_name!r}..."
|
||||
)
|
||||
queryset = queryset.exclude(uuid=chosen.uuid)
|
||||
queryset.delete()
|
||||
return chosen
|
||||
|
|
@ -254,7 +269,9 @@ class AbstractVendor:
|
|||
def auto_resolve_category(self, category_name: str = "") -> Category | None:
|
||||
if category_name:
|
||||
try:
|
||||
search = process_system_query(query=category_name, indexes=("categories",))
|
||||
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)
|
||||
|
|
@ -308,7 +325,9 @@ class AbstractVendor:
|
|||
|
||||
return round(price, 2)
|
||||
|
||||
def resolve_price_with_currency(self, price: float | int | Decimal, provider: str, currency: str = "") -> float:
|
||||
def resolve_price_with_currency(
|
||||
self, price: float | int | Decimal, provider: str, currency: str = ""
|
||||
) -> float:
|
||||
if all([not currency, not self.currency]):
|
||||
raise ValueError("Currency must be provided.")
|
||||
|
||||
|
|
@ -320,7 +339,9 @@ class AbstractVendor:
|
|||
rate = rates.get(currency or self.currency) if rates else 1
|
||||
|
||||
if not rate:
|
||||
raise RatesError(f"No rate found for {currency} in {rates} with probider {provider}...")
|
||||
raise RatesError(
|
||||
f"No rate found for {currency} in {rates} with probider {provider}..."
|
||||
)
|
||||
|
||||
return float(round(price / rate, 2)) if rate else float(round(price, 2)) # type: ignore [arg-type, operator]
|
||||
|
||||
|
|
@ -368,16 +389,22 @@ class AbstractVendor:
|
|||
except Vendor.DoesNotExist as dne:
|
||||
if safe:
|
||||
return None
|
||||
raise Exception(f"No matching vendor found with name {self.vendor_name!r}...") from dne
|
||||
raise Exception(
|
||||
f"No matching vendor found with name {self.vendor_name!r}..."
|
||||
) from dne
|
||||
|
||||
def get_products(self) -> None:
|
||||
pass
|
||||
|
||||
def get_products_queryset(self) -> QuerySet[Product]:
|
||||
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]:
|
||||
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]:
|
||||
return AttributeValue.objects.filter(
|
||||
|
|
@ -400,16 +427,22 @@ class AbstractVendor:
|
|||
case _:
|
||||
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:
|
||||
case "deactivate":
|
||||
filter_kwargs: dict[str, Any] = {"is_active": False}
|
||||
case "description":
|
||||
filter_kwargs: dict[str, Any] = {"description__exact": "EVIBES_DELETED_PRODUCT"}
|
||||
filter_kwargs: dict[str, Any] = {
|
||||
"description__exact": "EVIBES_DELETED_PRODUCT"
|
||||
}
|
||||
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...")
|
||||
|
|
@ -420,7 +453,9 @@ class AbstractVendor:
|
|||
if products is None:
|
||||
return
|
||||
|
||||
batch_ids = list(products.filter(**filter_kwargs).values_list("pk", flat=True)[:size])
|
||||
batch_ids = list(
|
||||
products.filter(**filter_kwargs).values_list("pk", flat=True)[:size]
|
||||
)
|
||||
if not batch_ids:
|
||||
break
|
||||
with suppress(Exception):
|
||||
|
|
@ -433,7 +468,9 @@ class AbstractVendor:
|
|||
self.get_stocks_queryset().delete()
|
||||
self.get_attribute_values_queryset().delete()
|
||||
|
||||
def get_or_create_attribute_safe(self, *, name: str, attr_group: AttributeGroup) -> Attribute:
|
||||
def get_or_create_attribute_safe(
|
||||
self, *, name: str, attr_group: AttributeGroup
|
||||
) -> Attribute:
|
||||
key = name[:255]
|
||||
try:
|
||||
attr = Attribute.objects.get(name=key)
|
||||
|
|
@ -459,15 +496,22 @@ class AbstractVendor:
|
|||
self, key: str, value: Any, product: Product, attr_group: AttributeGroup
|
||||
) -> AttributeValue | None:
|
||||
self.log(
|
||||
LogLevel.DEBUG, f"Trying to save attribute {key} with value {value} to {attr_group.name} of {product.pk}"
|
||||
LogLevel.DEBUG,
|
||||
f"Trying to save attribute {key} with value {value} to {attr_group.name} of {product.pk}",
|
||||
)
|
||||
|
||||
if not value:
|
||||
self.log(LogLevel.WARNING, f"No value for attribute {key!r} at {product.name!r}...")
|
||||
self.log(
|
||||
LogLevel.WARNING,
|
||||
f"No value for attribute {key!r} at {product.name!r}...",
|
||||
)
|
||||
return None
|
||||
|
||||
if not attr_group:
|
||||
self.log(LogLevel.WARNING, f"No group for attribute {key!r} at {product.name!r}...")
|
||||
self.log(
|
||||
LogLevel.WARNING,
|
||||
f"No group for attribute {key!r} at {product.name!r}...",
|
||||
)
|
||||
return None
|
||||
|
||||
if key in self.blocked_attributes:
|
||||
|
|
@ -488,7 +532,11 @@ class AbstractVendor:
|
|||
defaults={"is_active": True},
|
||||
)
|
||||
except Attribute.MultipleObjectsReturned:
|
||||
attribute = Attribute.objects.filter(name=key, group=attr_group).order_by("uuid").first() # type: ignore [assignment]
|
||||
attribute = (
|
||||
Attribute.objects.filter(name=key, group=attr_group)
|
||||
.order_by("uuid")
|
||||
.first()
|
||||
) # type: ignore [assignment]
|
||||
fields_to_update: list[str] = []
|
||||
if not attribute.is_active:
|
||||
attribute.is_active = True
|
||||
|
|
@ -507,7 +555,10 @@ class AbstractVendor:
|
|||
continue
|
||||
raise
|
||||
except IntegrityError:
|
||||
self.log(LogLevel.WARNING, f"IntegrityError while processing attribute {key!r}...")
|
||||
self.log(
|
||||
LogLevel.WARNING,
|
||||
f"IntegrityError while processing attribute {key!r}...",
|
||||
)
|
||||
return None
|
||||
|
||||
if not is_created:
|
||||
|
|
|
|||
|
|
@ -12,8 +12,15 @@ from django.contrib.sitemaps.views import index as _sitemap_index_view
|
|||
from django.contrib.sitemaps.views import sitemap as _sitemap_detail_view
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import BadRequest
|
||||
from django.db.models import Count, Sum, F
|
||||
from django.http import FileResponse, Http404, HttpRequest, HttpResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.db.models import Count, F, Sum
|
||||
from django.http import (
|
||||
FileResponse,
|
||||
Http404,
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
HttpResponseRedirect,
|
||||
JsonResponse,
|
||||
)
|
||||
from django.shortcuts import redirect
|
||||
from django.template import Context
|
||||
from django.urls import reverse
|
||||
|
|
@ -27,7 +34,11 @@ from django_ratelimit.decorators import ratelimit
|
|||
from djangorestframework_camel_case.render import CamelCaseJSONRenderer
|
||||
from djangorestframework_camel_case.util import camelize
|
||||
from drf_spectacular.utils import extend_schema_view
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
SpectacularRedocView,
|
||||
SpectacularSwaggerView,
|
||||
)
|
||||
from graphene_file_upload.django import FileUploadGraphQLView
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
|
@ -51,7 +62,13 @@ from engine.core.docs.drf.views import (
|
|||
SEARCH_SCHEMA,
|
||||
)
|
||||
from engine.core.elasticsearch import process_query
|
||||
from engine.core.models import DigitalAssetDownload, Order, OrderProduct, Product, Wishlist
|
||||
from engine.core.models import (
|
||||
DigitalAssetDownload,
|
||||
Order,
|
||||
OrderProduct,
|
||||
Product,
|
||||
Wishlist,
|
||||
)
|
||||
from engine.core.serializers import (
|
||||
BuyAsBusinessOrderSerializer,
|
||||
CacheOperatorSerializer,
|
||||
|
|
@ -138,7 +155,9 @@ class CustomRedocView(SpectacularRedocView):
|
|||
|
||||
@extend_schema_view(**LANGUAGE_SCHEMA)
|
||||
class SupportedLanguagesView(APIView):
|
||||
__doc__ = _("Returns a list of supported languages and their corresponding information.") # type: ignore [assignment]
|
||||
__doc__ = _(
|
||||
"Returns a list of supported languages and their corresponding information."
|
||||
) # type: ignore [assignment]
|
||||
|
||||
serializer_class = LanguageSerializer
|
||||
permission_classes = [
|
||||
|
|
@ -184,12 +203,16 @@ class WebsiteParametersView(APIView):
|
|||
]
|
||||
|
||||
def get(self, request: Request, *args, **kwargs) -> Response:
|
||||
return Response(data=camelize(get_project_parameters()), status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
data=camelize(get_project_parameters()), status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_view(**CACHE_SCHEMA)
|
||||
class CacheOperatorView(APIView):
|
||||
__doc__ = _("Handles cache operations such as reading and setting cache data with a specified key and timeout.") # type: ignore [assignment]
|
||||
__doc__ = _(
|
||||
"Handles cache operations such as reading and setting cache data with a specified key and timeout."
|
||||
) # type: ignore [assignment]
|
||||
|
||||
serializer_class = CacheOperatorSerializer
|
||||
permission_classes = [
|
||||
|
|
@ -237,7 +260,9 @@ class ContactUsView(APIView):
|
|||
|
||||
@extend_schema_view(**REQUEST_CURSED_URL_SCHEMA)
|
||||
class RequestCursedURLView(APIView):
|
||||
__doc__ = _("Handles requests for processing and validating URLs from incoming POST requests.") # type: ignore [assignment]
|
||||
__doc__ = _(
|
||||
"Handles requests for processing and validating URLs from incoming POST requests."
|
||||
) # type: ignore [assignment]
|
||||
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
|
|
@ -260,7 +285,9 @@ class RequestCursedURLView(APIView):
|
|||
try:
|
||||
data = cache.get(url, None)
|
||||
if not data:
|
||||
response = requests.get(str(url), headers={"content-type": "application/json"})
|
||||
response = requests.get(
|
||||
str(url), headers={"content-type": "application/json"}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = camelize(response.json())
|
||||
cache.set(url, data, 86400)
|
||||
|
|
@ -287,7 +314,15 @@ class GlobalSearchView(APIView):
|
|||
]
|
||||
|
||||
def get(self, request: Request, *args, **kwargs) -> Response:
|
||||
return Response(camelize({"results": process_query(query=request.GET.get("q", "").strip(), request=request)}))
|
||||
return Response(
|
||||
camelize(
|
||||
{
|
||||
"results": process_query(
|
||||
query=request.GET.get("q", "").strip(), request=request
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_view(**BUY_AS_BUSINESS_SCHEMA)
|
||||
|
|
@ -295,21 +330,32 @@ class BuyAsBusinessView(APIView):
|
|||
__doc__ = _("Handles the logic of buying as a business without registration.") # type: ignore [assignment]
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@method_decorator(ratelimit(key="ip", rate="10/h" if not settings.DEBUG else "888/h"))
|
||||
@method_decorator(
|
||||
ratelimit(key="ip", rate="10/h" if not settings.DEBUG else "888/h")
|
||||
)
|
||||
def post(self, request: Request, *args, **kwargs) -> Response:
|
||||
serializer = BuyAsBusinessOrderSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
order = Order.objects.create(status="MOMENTAL")
|
||||
products = [product.get("product_uuid") for product in serializer.validated_data.get("products")]
|
||||
products = [
|
||||
product.get("product_uuid")
|
||||
for product in serializer.validated_data.get("products")
|
||||
]
|
||||
try:
|
||||
transaction = order.buy_without_registration(
|
||||
products=products,
|
||||
promocode_uuid=serializer.validated_data.get("promocode_uuid"),
|
||||
customer_name=serializer.validated_data.get("business_identificator"),
|
||||
customer_email=serializer.validated_data.get("business_email"),
|
||||
customer_phone_number=serializer.validated_data.get("business_phone_number"),
|
||||
billing_customer_address=serializer.validated_data.get("billing_business_address_uuid"),
|
||||
shipping_customer_address=serializer.validated_data.get("shipping_business_address_uuid"),
|
||||
customer_phone_number=serializer.validated_data.get(
|
||||
"business_phone_number"
|
||||
),
|
||||
billing_customer_address=serializer.validated_data.get(
|
||||
"billing_business_address_uuid"
|
||||
),
|
||||
shipping_customer_address=serializer.validated_data.get(
|
||||
"shipping_business_address_uuid"
|
||||
),
|
||||
payment_method=serializer.validated_data.get("payment_method"),
|
||||
is_business=True,
|
||||
)
|
||||
|
|
@ -353,7 +399,9 @@ class DownloadDigitalAssetView(APIView):
|
|||
raise BadRequest(_("you can only download the digital asset once"))
|
||||
|
||||
if order_product.download.order_product.status != "FINISHED":
|
||||
raise BadRequest(_("the order must be paid before downloading the digital asset"))
|
||||
raise BadRequest(
|
||||
_("the order must be paid before downloading the digital asset")
|
||||
)
|
||||
|
||||
order_product.download.num_downloads += 1
|
||||
order_product.download.save()
|
||||
|
|
@ -373,10 +421,15 @@ class DownloadDigitalAssetView(APIView):
|
|||
return response
|
||||
|
||||
except BadRequest as e:
|
||||
return Response(data=camelize({"error": str(e)}), status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
data=camelize({"error": str(e)}), status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
except DigitalAssetDownload.DoesNotExist:
|
||||
return Response(data=camelize({"error": "Digital asset not found"}), status=status.HTTP_404_NOT_FOUND)
|
||||
return Response(
|
||||
data=camelize({"error": "Digital asset not found"}),
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
|
|
@ -457,7 +510,9 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
result = 0.0
|
||||
with suppress(Exception):
|
||||
qs = (
|
||||
OrderProduct.objects.filter(status__in=["FINISHED"], order__status="FINISHED")
|
||||
OrderProduct.objects.filter(
|
||||
status__in=["FINISHED"], order__status="FINISHED"
|
||||
)
|
||||
.filter(order__buy_time__lt=end, order__buy_time__gte=start)
|
||||
.aggregate(total=Sum(F("buy_price") * F("quantity")))
|
||||
)
|
||||
|
|
@ -480,7 +535,9 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
def count_finished_orders_between(start: date | None, end: date | None) -> int:
|
||||
result = 0
|
||||
with suppress(Exception):
|
||||
result = Order.objects.filter(status="FINISHED", buy_time__lt=end, buy_time__gte=start).count()
|
||||
result = Order.objects.filter(
|
||||
status="FINISHED", buy_time__lt=end, buy_time__gte=start
|
||||
).count()
|
||||
return result
|
||||
|
||||
revenue_gross_prev = sum_gross_between(prev_start, prev_end)
|
||||
|
|
@ -498,7 +555,9 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
else:
|
||||
if tax_included:
|
||||
divisor = 1.0 + (tax_rate / 100.0)
|
||||
revenue_net_prev = revenue_gross_prev / divisor if divisor > 0 else revenue_gross_prev
|
||||
revenue_net_prev = (
|
||||
revenue_gross_prev / divisor if divisor > 0 else revenue_gross_prev
|
||||
)
|
||||
else:
|
||||
revenue_net_prev = revenue_gross_prev
|
||||
revenue_net_prev = round(float(revenue_net_prev or 0.0), 2)
|
||||
|
|
@ -513,11 +572,27 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
result = round(((cur_f - prev_f) / prev_f) * 100.0, 1)
|
||||
return result
|
||||
|
||||
aov_cur: float = round((revenue_gross_cur / orders_finished_cur), 2) if orders_finished_cur > 0 else 0.0
|
||||
refund_rate_cur: float = round(((returns_cur / revenue_gross_cur) * 100.0), 1) if revenue_gross_cur > 0 else 0.0
|
||||
aov_cur: float = (
|
||||
round((revenue_gross_cur / orders_finished_cur), 2)
|
||||
if orders_finished_cur > 0
|
||||
else 0.0
|
||||
)
|
||||
refund_rate_cur: float = (
|
||||
round(((returns_cur / revenue_gross_cur) * 100.0), 1)
|
||||
if revenue_gross_cur > 0
|
||||
else 0.0
|
||||
)
|
||||
|
||||
aov_prev: float = round((revenue_gross_prev / orders_finished_prev), 2) if orders_finished_prev > 0 else 0.0
|
||||
refund_rate_prev: float = round(((returns_prev / revenue_gross_prev) * 100.0), 1) if revenue_gross_prev > 0 else 0.0
|
||||
aov_prev: float = (
|
||||
round((revenue_gross_prev / orders_finished_prev), 2)
|
||||
if orders_finished_prev > 0
|
||||
else 0.0
|
||||
)
|
||||
refund_rate_prev: float = (
|
||||
round(((returns_prev / revenue_gross_prev) * 100.0), 1)
|
||||
if revenue_gross_prev > 0
|
||||
else 0.0
|
||||
)
|
||||
|
||||
kpi = {
|
||||
"gmv": {
|
||||
|
|
@ -530,7 +605,11 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
"prev": orders_finished_prev,
|
||||
"delta_pct": pct_delta(orders_finished_cur, orders_finished_prev),
|
||||
},
|
||||
"aov": {"value": aov_cur, "prev": aov_prev, "delta_pct": pct_delta(aov_cur, aov_prev)},
|
||||
"aov": {
|
||||
"value": aov_cur,
|
||||
"prev": aov_prev,
|
||||
"delta_pct": pct_delta(aov_cur, aov_prev),
|
||||
},
|
||||
"net": {
|
||||
"value": revenue_net_cur,
|
||||
"prev": revenue_net_prev,
|
||||
|
|
@ -550,7 +629,9 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
|
||||
quick_links: list[dict[str, str]] = []
|
||||
with suppress(Exception):
|
||||
quick_links_section = settings.UNFOLD.get("SIDEBAR", {}).get("navigation", [])[1] # type: ignore[assignment]
|
||||
quick_links_section = settings.UNFOLD.get("SIDEBAR", {}).get("navigation", [])[
|
||||
1
|
||||
] # type: ignore[assignment]
|
||||
for item in quick_links_section.get("items", []):
|
||||
title = item.get("title")
|
||||
link = item.get("link")
|
||||
|
|
@ -578,16 +659,24 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
if wished_first and wished_first.get("products"):
|
||||
product = Product.objects.filter(pk=wished_first["products"]).first()
|
||||
if product:
|
||||
img = product.images.first().image_url if product.images.exists() else "" # type: ignore [union-attr]
|
||||
img = (
|
||||
product.images.first().image_url if product.images.exists() else ""
|
||||
) # type: ignore [union-attr]
|
||||
most_wished = {
|
||||
"name": product.name,
|
||||
"image": img,
|
||||
"admin_url": reverse("admin:core_product_change", args=[product.pk]),
|
||||
"admin_url": reverse(
|
||||
"admin:core_product_change", args=[product.pk]
|
||||
),
|
||||
}
|
||||
|
||||
wished_top10 = list(wished_qs[:10])
|
||||
if wished_top10:
|
||||
counts_map = {row["products"]: row["cnt"] for row in wished_top10 if row.get("products")}
|
||||
counts_map = {
|
||||
row["products"]: row["cnt"]
|
||||
for row in wished_top10
|
||||
if row.get("products")
|
||||
}
|
||||
products = Product.objects.filter(pk__in=counts_map.keys())
|
||||
product_by_id = {p.pk: p for p in products}
|
||||
for row in wished_top10:
|
||||
|
|
@ -620,7 +709,9 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
context["daily_labels"] = labels
|
||||
context["daily_orders"] = orders_series
|
||||
context["daily_gross"] = gross_series
|
||||
context["daily_title"] = _("Revenue & Orders (last %(days)d)") % {"days": period_days}
|
||||
context["daily_title"] = _("Revenue & Orders (last %(days)d)") % {
|
||||
"days": period_days
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("Failed to build daily stats: %s", e)
|
||||
context["daily_labels"] = []
|
||||
|
|
@ -633,7 +724,9 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
context["daily_labels"] = [d.strftime("%d %b") for d in date_axis]
|
||||
context["daily_orders"] = [0 for _i in date_axis]
|
||||
context["daily_gross"] = [0.0 for _j in date_axis]
|
||||
context["daily_title"] = _("Revenue & Orders (last %(days)d)") % {"days": period_days}
|
||||
context["daily_title"] = _("Revenue & Orders (last %(days)d)") % {
|
||||
"days": period_days
|
||||
}
|
||||
|
||||
low_stock_list: list[dict[str, str | int]] = []
|
||||
with suppress(Exception):
|
||||
|
|
@ -649,7 +742,9 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
"name": str(p.get("name") or ""),
|
||||
"sku": str(p.get("sku") or ""),
|
||||
"qty": qty,
|
||||
"admin_url": reverse("admin:core_product_change", args=[p.get("id")]),
|
||||
"admin_url": reverse(
|
||||
"admin:core_product_change", args=[p.get("id")]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -675,7 +770,9 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
most_popular_list: list[dict[str, str | int | float | None]] = []
|
||||
with suppress(Exception):
|
||||
popular_qs = (
|
||||
OrderProduct.objects.filter(status="FINISHED", order__status="FINISHED", product__isnull=False)
|
||||
OrderProduct.objects.filter(
|
||||
status="FINISHED", order__status="FINISHED", product__isnull=False
|
||||
)
|
||||
.values("product")
|
||||
.annotate(total_qty=Sum("quantity"))
|
||||
.order_by("-total_qty")
|
||||
|
|
@ -684,16 +781,24 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
if popular_first and popular_first.get("product"):
|
||||
product = Product.objects.filter(pk=popular_first["product"]).first()
|
||||
if product:
|
||||
img = product.images.first().image_url if product.images.exists() else "" # type: ignore [union-attr]
|
||||
img = (
|
||||
product.images.first().image_url if product.images.exists() else ""
|
||||
) # type: ignore [union-attr]
|
||||
most_popular = {
|
||||
"name": product.name,
|
||||
"image": img,
|
||||
"admin_url": reverse("admin:core_product_change", args=[product.pk]),
|
||||
"admin_url": reverse(
|
||||
"admin:core_product_change", args=[product.pk]
|
||||
),
|
||||
}
|
||||
|
||||
popular_top10 = list(popular_qs[:10])
|
||||
if popular_top10:
|
||||
qty_map = {row["product"]: row["total_qty"] for row in popular_top10 if row.get("product")}
|
||||
qty_map = {
|
||||
row["product"]: row["total_qty"]
|
||||
for row in popular_top10
|
||||
if row.get("product")
|
||||
}
|
||||
products = Product.objects.filter(pk__in=qty_map.keys())
|
||||
product_by_id = {p.pk: p for p in products}
|
||||
for row in popular_top10:
|
||||
|
|
@ -711,7 +816,12 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
}
|
||||
)
|
||||
|
||||
customers_mix: dict[str, int | float] = {"new": 0, "returning": 0, "new_pct": 0.0, "returning_pct": 0.0}
|
||||
customers_mix: dict[str, int | float] = {
|
||||
"new": 0,
|
||||
"returning": 0,
|
||||
"new_pct": 0.0,
|
||||
"returning_pct": 0.0,
|
||||
}
|
||||
with suppress(Exception):
|
||||
mix = get_customer_mix()
|
||||
n = int(mix.get("new", 0))
|
||||
|
|
@ -719,7 +829,13 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
t = max(n + r, 0)
|
||||
new_pct = round((n / t * 100.0), 1) if t > 0 else 0.0
|
||||
ret_pct = round((r / t * 100.0), 1) if t > 0 else 0.0
|
||||
customers_mix = {"new": n, "returning": r, "new_pct": new_pct, "returning_pct": ret_pct, "total": t}
|
||||
customers_mix = {
|
||||
"new": n,
|
||||
"returning": r,
|
||||
"new_pct": new_pct,
|
||||
"returning_pct": ret_pct,
|
||||
"total": t,
|
||||
}
|
||||
|
||||
shipped_vs_digital: dict[str, int | float] = {
|
||||
"digital_qty": 0,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,14 @@ from engine.core.docs.drf.viewsets import (
|
|||
VENDOR_SCHEMA,
|
||||
WISHLIST_SCHEMA,
|
||||
)
|
||||
from engine.core.filters import AddressFilter, BrandFilter, CategoryFilter, FeedbackFilter, OrderFilter, ProductFilter
|
||||
from engine.core.filters import (
|
||||
AddressFilter,
|
||||
BrandFilter,
|
||||
CategoryFilter,
|
||||
FeedbackFilter,
|
||||
OrderFilter,
|
||||
ProductFilter,
|
||||
)
|
||||
from engine.core.models import (
|
||||
Address,
|
||||
Attribute,
|
||||
|
|
@ -143,11 +150,18 @@ class EvibesViewSet(ModelViewSet):
|
|||
action_serializer_classes: dict[str, Type[Serializer]] = {}
|
||||
additional: dict[str, str] = {}
|
||||
permission_classes = [EvibesPermission]
|
||||
renderer_classes = [CamelCaseJSONRenderer, MultiPartRenderer, XMLRenderer, YAMLRenderer]
|
||||
renderer_classes = [
|
||||
CamelCaseJSONRenderer,
|
||||
MultiPartRenderer,
|
||||
XMLRenderer,
|
||||
YAMLRenderer,
|
||||
]
|
||||
|
||||
def get_serializer_class(self) -> Type[Serializer]:
|
||||
# noinspection PyTypeChecker
|
||||
return self.action_serializer_classes.get(self.action, super().get_serializer_class()) # type: ignore [arg-type]
|
||||
return self.action_serializer_classes.get(
|
||||
self.action, super().get_serializer_class()
|
||||
) # type: ignore [arg-type]
|
||||
|
||||
|
||||
@extend_schema_view(**ATTRIBUTE_GROUP_SCHEMA)
|
||||
|
|
@ -272,7 +286,11 @@ class CategoryViewSet(EvibesViewSet):
|
|||
title = f"{category.name} | {settings.PROJECT_NAME}"
|
||||
description = (category.description or "")[:180]
|
||||
canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{category.slug}"
|
||||
og_image = request.build_absolute_uri(category.image.url) if getattr(category, "image", None) else ""
|
||||
og_image = (
|
||||
request.build_absolute_uri(category.image.url)
|
||||
if getattr(category, "image", None)
|
||||
else ""
|
||||
)
|
||||
|
||||
og = {
|
||||
"title": title,
|
||||
|
|
@ -286,10 +304,20 @@ class CategoryViewSet(EvibesViewSet):
|
|||
crumbs = [("Home", f"https://{settings.BASE_DOMAIN}/")]
|
||||
if category.get_ancestors().exists():
|
||||
for c in category.get_ancestors():
|
||||
crumbs.append((c.name, f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{c.slug}"))
|
||||
crumbs.append(
|
||||
(
|
||||
c.name,
|
||||
f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{c.slug}",
|
||||
)
|
||||
)
|
||||
crumbs.append((category.name, canonical))
|
||||
|
||||
json_ld = [org_schema(), website_schema(), breadcrumb_schema(crumbs), category_schema(category, canonical)]
|
||||
json_ld = [
|
||||
org_schema(),
|
||||
website_schema(),
|
||||
breadcrumb_schema(crumbs),
|
||||
category_schema(category, canonical),
|
||||
]
|
||||
|
||||
product_urls = []
|
||||
qs = (
|
||||
|
|
@ -303,7 +331,9 @@ class CategoryViewSet(EvibesViewSet):
|
|||
.distinct()[:24]
|
||||
)
|
||||
for p in qs:
|
||||
product_urls.append(f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/product/{p.slug}")
|
||||
product_urls.append(
|
||||
f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/product/{p.slug}"
|
||||
)
|
||||
if product_urls:
|
||||
json_ld.append(item_list_schema(product_urls))
|
||||
|
||||
|
|
@ -443,7 +473,9 @@ class ProductViewSet(EvibesViewSet):
|
|||
"related feedback of a product."
|
||||
)
|
||||
|
||||
queryset = Product.objects.prefetch_related("tags", "attributes", "stocks", "images").all()
|
||||
queryset = Product.objects.prefetch_related(
|
||||
"tags", "attributes", "stocks", "images"
|
||||
).all()
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_class = ProductFilter
|
||||
serializer_class = ProductDetailSerializer
|
||||
|
|
@ -466,7 +498,9 @@ class ProductViewSet(EvibesViewSet):
|
|||
if self.request.user.has_perm("core.view_product"):
|
||||
return qs
|
||||
|
||||
active_stocks = Stock.objects.filter(product_id=OuterRef("pk"), vendor__is_active=True)
|
||||
active_stocks = Stock.objects.filter(
|
||||
product_id=OuterRef("pk"), vendor__is_active=True
|
||||
)
|
||||
|
||||
return (
|
||||
qs.filter(
|
||||
|
|
@ -530,7 +564,9 @@ class ProductViewSet(EvibesViewSet):
|
|||
rating = {"value": p.rating, "count": p.feedbacks_count}
|
||||
title = f"{p.name} | {settings.PROJECT_NAME}"
|
||||
description = (p.description or "")[:180]
|
||||
canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/product/{p.slug}"
|
||||
canonical = (
|
||||
f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/product/{p.slug}"
|
||||
)
|
||||
og = {
|
||||
"title": title,
|
||||
"description": description,
|
||||
|
|
@ -543,7 +579,12 @@ class ProductViewSet(EvibesViewSet):
|
|||
crumbs = [("Home", f"https://{settings.BASE_DOMAIN}/")]
|
||||
if p.category:
|
||||
for c in p.category.get_ancestors(include_self=True):
|
||||
crumbs.append((c.name, f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{c.slug}"))
|
||||
crumbs.append(
|
||||
(
|
||||
c.name,
|
||||
f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{c.slug}",
|
||||
)
|
||||
)
|
||||
crumbs.append((p.name, canonical))
|
||||
|
||||
json_ld = [org_schema(), website_schema()]
|
||||
|
|
@ -642,7 +683,9 @@ class OrderViewSet(EvibesViewSet):
|
|||
additional = {"retrieve": "ALLOW"}
|
||||
|
||||
def get_serializer_class(self):
|
||||
return self.action_serializer_classes.get(self.action, super().get_serializer_class())
|
||||
return self.action_serializer_classes.get(
|
||||
self.action, super().get_serializer_class()
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
|
|
@ -705,19 +748,34 @@ class OrderViewSet(EvibesViewSet):
|
|||
)
|
||||
match str(type(instance)):
|
||||
case "<class 'engine.payments.models.Transaction'>":
|
||||
return Response(status=status.HTTP_202_ACCEPTED, data=TransactionProcessSerializer(instance).data)
|
||||
return Response(
|
||||
status=status.HTTP_202_ACCEPTED,
|
||||
data=TransactionProcessSerializer(instance).data,
|
||||
)
|
||||
case "<class 'engine.core.models.Order'>":
|
||||
return Response(status=status.HTTP_200_OK, data=OrderDetailSerializer(instance).data)
|
||||
return Response(
|
||||
status=status.HTTP_200_OK,
|
||||
data=OrderDetailSerializer(instance).data,
|
||||
)
|
||||
case _:
|
||||
raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}"))
|
||||
raise TypeError(
|
||||
_(
|
||||
f"wrong type came from order.buy() method: {type(instance)!s}"
|
||||
)
|
||||
)
|
||||
except Order.DoesNotExist:
|
||||
name = "Order"
|
||||
return Response(status=status.HTTP_404_NOT_FOUND, data={"detail": _(f"{name} does not exist: {uuid}")})
|
||||
return Response(
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
data={"detail": _(f"{name} does not exist: {uuid}")},
|
||||
)
|
||||
except Exception as e:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(e)})
|
||||
|
||||
@action(detail=False, methods=["post"], url_path="buy_unregistered")
|
||||
@method_decorator(ratelimit(key="ip", rate="10/h" if not settings.DEBUG else "888/h"))
|
||||
@method_decorator(
|
||||
ratelimit(key="ip", rate="10/h" if not settings.DEBUG else "888/h")
|
||||
)
|
||||
def buy_unregistered(self, request: Request, *args, **kwargs) -> Response:
|
||||
serializer = BuyUnregisteredOrderSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
|
@ -729,12 +787,21 @@ class OrderViewSet(EvibesViewSet):
|
|||
promocode_uuid=serializer.validated_data.get("promocode_uuid"),
|
||||
customer_name=serializer.validated_data.get("customer_name"),
|
||||
customer_email=serializer.validated_data.get("customer_email"),
|
||||
customer_phone_number=serializer.validated_data.get("customer_phone_number"),
|
||||
billing_customer_address=serializer.validated_data.get("billing_customer_address_uuid"),
|
||||
shipping_customer_address=serializer.validated_data.get("shipping_customer_address_uuid"),
|
||||
customer_phone_number=serializer.validated_data.get(
|
||||
"customer_phone_number"
|
||||
),
|
||||
billing_customer_address=serializer.validated_data.get(
|
||||
"billing_customer_address_uuid"
|
||||
),
|
||||
shipping_customer_address=serializer.validated_data.get(
|
||||
"shipping_customer_address_uuid"
|
||||
),
|
||||
payment_method=serializer.validated_data.get("payment_method"),
|
||||
)
|
||||
return Response(status=status.HTTP_201_CREATED, data=TransactionProcessSerializer(transaction).data)
|
||||
return Response(
|
||||
status=status.HTTP_201_CREATED,
|
||||
data=TransactionProcessSerializer(transaction).data,
|
||||
)
|
||||
except Exception as e:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(e)})
|
||||
|
||||
|
|
@ -745,18 +812,27 @@ class OrderViewSet(EvibesViewSet):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
order = self.get_object()
|
||||
if not (request.user.has_perm("core.add_orderproduct") or request.user == order.user):
|
||||
if not (
|
||||
request.user.has_perm("core.add_orderproduct")
|
||||
or request.user == order.user
|
||||
):
|
||||
raise PermissionDenied(permission_denied_message)
|
||||
|
||||
order = order.add_product(
|
||||
product_uuid=serializer.validated_data.get("product_uuid"),
|
||||
attributes=format_attributes(serializer.validated_data.get("attributes")),
|
||||
attributes=format_attributes(
|
||||
serializer.validated_data.get("attributes")
|
||||
),
|
||||
)
|
||||
return Response(
|
||||
status=status.HTTP_200_OK, data=OrderDetailSerializer(order).data
|
||||
)
|
||||
return Response(status=status.HTTP_200_OK, data=OrderDetailSerializer(order).data)
|
||||
except Order.DoesNotExist as dne:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND, data={"detail": str(dne)})
|
||||
except ValueError as ve:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(ve)})
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST, data={"detail": str(ve)}
|
||||
)
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="remove_order_product")
|
||||
@method_decorator(ratelimit(key="ip", rate="1/s" if not settings.DEBUG else "44/s"))
|
||||
|
|
@ -765,18 +841,27 @@ class OrderViewSet(EvibesViewSet):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
order = self.get_object()
|
||||
if not (request.user.has_perm("core.delete_orderproduct") or request.user == order.user):
|
||||
if not (
|
||||
request.user.has_perm("core.delete_orderproduct")
|
||||
or request.user == order.user
|
||||
):
|
||||
raise PermissionDenied(permission_denied_message)
|
||||
|
||||
order = order.remove_product(
|
||||
product_uuid=serializer.validated_data.get("product_uuid"),
|
||||
attributes=format_attributes(serializer.validated_data.get("attributes")),
|
||||
attributes=format_attributes(
|
||||
serializer.validated_data.get("attributes")
|
||||
),
|
||||
)
|
||||
return Response(
|
||||
status=status.HTTP_200_OK, data=OrderDetailSerializer(order).data
|
||||
)
|
||||
return Response(status=status.HTTP_200_OK, data=OrderDetailSerializer(order).data)
|
||||
except Order.DoesNotExist as dne:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND, data={"detail": str(dne)})
|
||||
except ValueError as ve:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(ve)})
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST, data={"detail": str(ve)}
|
||||
)
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="bulk_add_order_products")
|
||||
@method_decorator(ratelimit(key="ip", rate="1/s" if not settings.DEBUG else "44/s"))
|
||||
|
|
@ -786,17 +871,24 @@ class OrderViewSet(EvibesViewSet):
|
|||
lookup_val = kwargs.get(self.lookup_field)
|
||||
try:
|
||||
order = Order.objects.get(uuid=str(lookup_val))
|
||||
if not (request.user.has_perm("core.add_orderproduct") or request.user == order.user):
|
||||
if not (
|
||||
request.user.has_perm("core.add_orderproduct")
|
||||
or request.user == order.user
|
||||
):
|
||||
raise PermissionDenied(permission_denied_message)
|
||||
|
||||
order = order.bulk_add_products(
|
||||
products=serializer.validated_data.get("products"),
|
||||
)
|
||||
return Response(status=status.HTTP_200_OK, data=OrderDetailSerializer(order).data)
|
||||
return Response(
|
||||
status=status.HTTP_200_OK, data=OrderDetailSerializer(order).data
|
||||
)
|
||||
except Order.DoesNotExist as dne:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND, data={"detail": str(dne)})
|
||||
except ValueError as ve:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(ve)})
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST, data={"detail": str(ve)}
|
||||
)
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="bulk_remove_order_products")
|
||||
@method_decorator(ratelimit(key="ip", rate="1/s" if not settings.DEBUG else "44/s"))
|
||||
|
|
@ -805,17 +897,24 @@ class OrderViewSet(EvibesViewSet):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
order = self.get_object()
|
||||
if not (request.user.has_perm("core.delete_orderproduct") or request.user == order.user):
|
||||
if not (
|
||||
request.user.has_perm("core.delete_orderproduct")
|
||||
or request.user == order.user
|
||||
):
|
||||
raise PermissionDenied(permission_denied_message)
|
||||
|
||||
order = order.bulk_remove_products(
|
||||
products=serializer.validated_data.get("products"),
|
||||
)
|
||||
return Response(status=status.HTTP_200_OK, data=OrderDetailSerializer(order).data)
|
||||
return Response(
|
||||
status=status.HTTP_200_OK, data=OrderDetailSerializer(order).data
|
||||
)
|
||||
except Order.DoesNotExist as dne:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND, data={"detail": str(dne)})
|
||||
except ValueError as ve:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(ve)})
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST, data={"detail": str(ve)}
|
||||
)
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
|
|
@ -856,7 +955,10 @@ class OrderProductViewSet(EvibesViewSet):
|
|||
order_product = OrderProduct.objects.get(uuid=str(kwargs.get("pk")))
|
||||
if not order_product.order:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
if not (request.user.has_perm("core.change_orderproduct") or request.user == order_product.order.user):
|
||||
if not (
|
||||
request.user.has_perm("core.change_orderproduct")
|
||||
or request.user == order_product.order.user
|
||||
):
|
||||
raise PermissionDenied(permission_denied_message)
|
||||
feedback = order_product.do_feedback(
|
||||
rating=serializer.validated_data.get("rating"),
|
||||
|
|
@ -865,7 +967,10 @@ class OrderProductViewSet(EvibesViewSet):
|
|||
)
|
||||
match serializer.validated_data.get("action"):
|
||||
case "add":
|
||||
return Response(data=FeedbackDetailSerializer(feedback).data, status=status.HTTP_201_CREATED)
|
||||
return Response(
|
||||
data=FeedbackDetailSerializer(feedback).data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
case "remove":
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
case _:
|
||||
|
|
@ -889,11 +994,21 @@ class ProductImageViewSet(EvibesViewSet):
|
|||
|
||||
@extend_schema_view(**PROMOCODE_SCHEMA)
|
||||
class PromoCodeViewSet(EvibesViewSet):
|
||||
__doc__ = _("Manages the retrieval and handling of PromoCode instances through various API actions.")
|
||||
__doc__ = _(
|
||||
"Manages the retrieval and handling of PromoCode instances through various API actions."
|
||||
)
|
||||
|
||||
queryset = PromoCode.objects.all()
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ["code", "discount_amount", "discount_percent", "start_time", "end_time", "used_on", "is_active"]
|
||||
filterset_fields = [
|
||||
"code",
|
||||
"discount_amount",
|
||||
"discount_percent",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"used_on",
|
||||
"is_active",
|
||||
]
|
||||
serializer_class = PromoCodeDetailSerializer
|
||||
action_serializer_classes = {
|
||||
"list": PromoCodeSimpleSerializer,
|
||||
|
|
@ -984,14 +1099,19 @@ class WishlistViewSet(EvibesViewSet):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
wishlist = self.get_object()
|
||||
if not (request.user.has_perm("core.change_wishlist") or request.user == wishlist.user):
|
||||
if not (
|
||||
request.user.has_perm("core.change_wishlist")
|
||||
or request.user == wishlist.user
|
||||
):
|
||||
raise PermissionDenied(permission_denied_message)
|
||||
|
||||
wishlist = wishlist.add_product(
|
||||
product_uuid=serializer.validated_data.get("product_uuid"),
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_200_OK, data=WishlistDetailSerializer(wishlist).data)
|
||||
return Response(
|
||||
status=status.HTTP_200_OK, data=WishlistDetailSerializer(wishlist).data
|
||||
)
|
||||
except Wishlist.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
|
@ -1002,14 +1122,19 @@ class WishlistViewSet(EvibesViewSet):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
wishlist = self.get_object()
|
||||
if not (request.user.has_perm("core.change_wishlist") or request.user == wishlist.user):
|
||||
if not (
|
||||
request.user.has_perm("core.change_wishlist")
|
||||
or request.user == wishlist.user
|
||||
):
|
||||
raise PermissionDenied(permission_denied_message)
|
||||
|
||||
wishlist = wishlist.remove_product(
|
||||
product_uuid=serializer.validated_data.get("product_uuid"),
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_200_OK, data=WishlistDetailSerializer(wishlist).data)
|
||||
return Response(
|
||||
status=status.HTTP_200_OK, data=WishlistDetailSerializer(wishlist).data
|
||||
)
|
||||
except Wishlist.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
|
@ -1020,32 +1145,44 @@ class WishlistViewSet(EvibesViewSet):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
wishlist = self.get_object()
|
||||
if not (request.user.has_perm("core.change_wishlist") or request.user == wishlist.user):
|
||||
if not (
|
||||
request.user.has_perm("core.change_wishlist")
|
||||
or request.user == wishlist.user
|
||||
):
|
||||
raise PermissionDenied(permission_denied_message)
|
||||
|
||||
wishlist = wishlist.bulk_add_products(
|
||||
product_uuids=serializer.validated_data.get("product_uuids"),
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_200_OK, data=WishlistDetailSerializer(wishlist).data)
|
||||
return Response(
|
||||
status=status.HTTP_200_OK, data=WishlistDetailSerializer(wishlist).data
|
||||
)
|
||||
except Order.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@action(detail=True, methods=["post"], url_path="bulk_remove_wishlist_product")
|
||||
def bulk_remove_wishlist_products(self, request: Request, *args, **kwargs) -> Response:
|
||||
def bulk_remove_wishlist_products(
|
||||
self, request: Request, *args, **kwargs
|
||||
) -> Response:
|
||||
serializer = BulkRemoveWishlistProductSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
wishlist = self.get_object()
|
||||
if not (request.user.has_perm("core.change_wishlist") or request.user == wishlist.user):
|
||||
if not (
|
||||
request.user.has_perm("core.change_wishlist")
|
||||
or request.user == wishlist.user
|
||||
):
|
||||
raise PermissionDenied(permission_denied_message)
|
||||
|
||||
wishlist = wishlist.bulk_remove_products(
|
||||
product_uuids=serializer.validated_data.get("product_uuids"),
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_200_OK, data=WishlistDetailSerializer(wishlist).data)
|
||||
return Response(
|
||||
status=status.HTTP_200_OK, data=WishlistDetailSerializer(wishlist).data
|
||||
)
|
||||
except Order.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
|
@ -1085,12 +1222,16 @@ class AddressViewSet(EvibesViewSet):
|
|||
def retrieve(self, request: Request, *args, **kwargs) -> Response:
|
||||
try:
|
||||
address = Address.objects.get(uuid=str(kwargs.get("pk")))
|
||||
return Response(status=status.HTTP_200_OK, data=self.get_serializer(address).data)
|
||||
return Response(
|
||||
status=status.HTTP_200_OK, data=self.get_serializer(address).data
|
||||
)
|
||||
except Address.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def create(self, request: Request, *args, **kwargs) -> Response:
|
||||
create_serializer = AddressCreateSerializer(data=request.data, context={"request": request})
|
||||
create_serializer = AddressCreateSerializer(
|
||||
data=request.data, context={"request": request}
|
||||
)
|
||||
create_serializer.is_valid(raise_exception=True)
|
||||
|
||||
address_obj = create_serializer.create(create_serializer.validated_data)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,10 @@ class JSONTableWidget(forms.Widget):
|
|||
return super().render(name, value, attrs, renderer)
|
||||
|
||||
def value_from_datadict(
|
||||
self, data: Mapping[str, Any], files: MultiValueDict[str, UploadedFile], name: str
|
||||
self,
|
||||
data: Mapping[str, Any],
|
||||
files: MultiValueDict[str, UploadedFile],
|
||||
name: str,
|
||||
) -> str | None:
|
||||
json_data = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ from drf_spectacular.utils import extend_schema
|
|||
from rest_framework import status
|
||||
|
||||
from engine.core.docs.drf import error
|
||||
from engine.payments.serializers import DepositSerializer, TransactionProcessSerializer, LimitsSerializer
|
||||
from engine.payments.serializers import (
|
||||
DepositSerializer,
|
||||
LimitsSerializer,
|
||||
TransactionProcessSerializer,
|
||||
)
|
||||
|
||||
DEPOSIT_SCHEMA = {
|
||||
"post": extend_schema(
|
||||
|
|
@ -27,7 +31,9 @@ LIMITS_SCHEMA = {
|
|||
"payments",
|
||||
],
|
||||
summary=_("payment limits"),
|
||||
description=_("retrieve minimal and maximal allowed deposit amounts across available gateways"),
|
||||
description=_(
|
||||
"retrieve minimal and maximal allowed deposit amounts across available gateways"
|
||||
),
|
||||
responses={
|
||||
status.HTTP_200_OK: LimitsSerializer,
|
||||
status.HTTP_401_UNAUTHORIZED: error,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||
from rest_framework import status
|
||||
|
||||
from engine.core.docs.drf import BASE_ERRORS
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ class Deposit(BaseMutation):
|
|||
def mutate(self, info, amount):
|
||||
if info.context.user.is_authenticated:
|
||||
transaction = Transaction.objects.create(
|
||||
balance=info.context.user.payments_balance, amount=amount, currency="EUR"
|
||||
balance=info.context.user.payments_balance,
|
||||
amount=amount,
|
||||
currency="EUR",
|
||||
)
|
||||
# noinspection PyTypeChecker
|
||||
return Deposit(transaction=transaction)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
from django.db.models import BooleanField, Case, F, Manager, Q, QuerySet, Sum, Value, When
|
||||
from django.db.models import (
|
||||
BooleanField,
|
||||
Case,
|
||||
F,
|
||||
Manager,
|
||||
Q,
|
||||
QuerySet,
|
||||
Sum,
|
||||
Value,
|
||||
When,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.timezone import now
|
||||
|
||||
|
|
@ -8,9 +18,18 @@ class GatewayQuerySet(QuerySet):
|
|||
today = now().date()
|
||||
current_month_start = today.replace(day=1)
|
||||
return self.annotate(
|
||||
daily_sum=Coalesce(Sum("transactions__amount", filter=Q(transactions__created__date=today)), Value(0.0)),
|
||||
daily_sum=Coalesce(
|
||||
Sum(
|
||||
"transactions__amount", filter=Q(transactions__created__date=today)
|
||||
),
|
||||
Value(0.0),
|
||||
),
|
||||
monthly_sum=Coalesce(
|
||||
Sum("transactions__amount", filter=Q(transactions__created__date__gte=current_month_start)), Value(0.0)
|
||||
Sum(
|
||||
"transactions__amount",
|
||||
filter=Q(transactions__created__date__gte=current_month_start),
|
||||
),
|
||||
Value(0.0),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,9 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
("amount", models.FloatField(default=0)),
|
||||
|
|
@ -86,13 +88,18 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
|
||||
auto_now=True,
|
||||
help_text="when the object was last modified",
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
("amount", models.FloatField()),
|
||||
("currency", models.CharField(max_length=3)),
|
||||
("payment_method", models.CharField(max_length=20)),
|
||||
("process", models.JSONField(default=dict, verbose_name="processing details")),
|
||||
(
|
||||
"process",
|
||||
models.JSONField(default=dict, verbose_name="processing details"),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "transaction",
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name="transaction",
|
||||
name="balance",
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="payments.balance"),
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="payments.balance"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="transaction",
|
||||
|
|
@ -44,6 +46,8 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
migrations.AddIndex(
|
||||
model_name="transaction",
|
||||
index=django.contrib.postgres.indexes.GinIndex(fields=["process"], name="payments_tr_process_d5b008_gin"),
|
||||
index=django.contrib.postgres.indexes.GinIndex(
|
||||
fields=["process"], name="payments_tr_process_d5b008_gin"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
import django_extensions.db.fields
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
|
@ -95,11 +96,15 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
(
|
||||
"minimum_transaction_amount",
|
||||
models.FloatField(default=0, verbose_name="minimum transaction amount"),
|
||||
models.FloatField(
|
||||
default=0, verbose_name="minimum transaction amount"
|
||||
),
|
||||
),
|
||||
(
|
||||
"maximum_transaction_amount",
|
||||
models.FloatField(default=0, verbose_name="maximum transaction amount"),
|
||||
models.FloatField(
|
||||
default=0, verbose_name="maximum transaction amount"
|
||||
),
|
||||
),
|
||||
(
|
||||
"daily_limit",
|
||||
|
|
@ -119,11 +124,15 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
(
|
||||
"priority",
|
||||
models.PositiveIntegerField(default=10, unique=True, verbose_name="priority"),
|
||||
models.PositiveIntegerField(
|
||||
default=10, unique=True, verbose_name="priority"
|
||||
),
|
||||
),
|
||||
(
|
||||
"integration_variables",
|
||||
models.JSONField(default=dict, verbose_name="integration variables"),
|
||||
models.JSONField(
|
||||
default=dict, verbose_name="integration variables"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ class Migration(migrations.Migration):
|
|||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="transaction",
|
||||
index=models.Index(fields=["created"], name="payments_tr_created_95e595_idx"),
|
||||
index=models.Index(
|
||||
fields=["created"], name="payments_tr_created_95e595_idx"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -25,7 +25,13 @@ from evibes.utils.misc import create_object
|
|||
|
||||
class Transaction(NiceModel):
|
||||
amount = FloatField(null=False, blank=False)
|
||||
balance = ForeignKey("payments.Balance", on_delete=CASCADE, blank=True, null=True, related_name="transactions")
|
||||
balance = ForeignKey(
|
||||
"payments.Balance",
|
||||
on_delete=CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="transactions",
|
||||
)
|
||||
currency = CharField(max_length=3, null=False, blank=False)
|
||||
payment_method = CharField(max_length=20, null=True, blank=True)
|
||||
order = ForeignKey(
|
||||
|
|
@ -37,7 +43,13 @@ class Transaction(NiceModel):
|
|||
related_name="payments_transactions",
|
||||
)
|
||||
process = JSONField(verbose_name=_("processing details"), default=dict)
|
||||
gateway = ForeignKey("payments.Gateway", on_delete=CASCADE, blank=True, null=True, related_name="transactions")
|
||||
gateway = ForeignKey(
|
||||
"payments.Gateway",
|
||||
on_delete=CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="transactions",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
|
|
@ -64,7 +76,11 @@ class Transaction(NiceModel):
|
|||
class Balance(NiceModel):
|
||||
amount = FloatField(null=False, blank=False, default=0)
|
||||
user = OneToOneField(
|
||||
to=settings.AUTH_USER_MODEL, on_delete=CASCADE, blank=True, null=True, related_name="payments_balance"
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
on_delete=CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="payments_balance",
|
||||
)
|
||||
transactions: QuerySet["Transaction"]
|
||||
|
||||
|
|
@ -122,8 +138,12 @@ class Gateway(NiceModel):
|
|||
verbose_name=_("monthly limit"),
|
||||
help_text=_("monthly sum limit of transactions' amounts. 0 means no limit"),
|
||||
)
|
||||
priority = PositiveIntegerField(null=False, blank=False, default=10, verbose_name=_("priority"), unique=True)
|
||||
integration_variables = JSONField(null=False, blank=False, default=dict, verbose_name=_("integration variables"))
|
||||
priority = PositiveIntegerField(
|
||||
null=False, blank=False, default=10, verbose_name=_("priority"), unique=True
|
||||
)
|
||||
integration_variables = JSONField(
|
||||
null=False, blank=False, default=dict, verbose_name=_("integration variables")
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
@ -140,18 +160,25 @@ class Gateway(NiceModel):
|
|||
today = timezone.localdate()
|
||||
|
||||
tz = timezone.get_current_timezone()
|
||||
month_start = timezone.make_aware(datetime.combine(today.replace(day=1), time.min), tz)
|
||||
month_start = timezone.make_aware(
|
||||
datetime.combine(today.replace(day=1), time.min), tz
|
||||
)
|
||||
if today.month == 12:
|
||||
next_month_date = today.replace(year=today.year + 1, month=1, day=1)
|
||||
else:
|
||||
next_month_date = today.replace(month=today.month + 1, day=1)
|
||||
month_end = timezone.make_aware(datetime.combine(next_month_date, time.min), tz)
|
||||
|
||||
daily_sum = self.transactions.filter(created__date=today).aggregate(total=Sum("amount"))["total"] or 0
|
||||
daily_sum = (
|
||||
self.transactions.filter(created__date=today).aggregate(
|
||||
total=Sum("amount")
|
||||
)["total"]
|
||||
or 0
|
||||
)
|
||||
monthly_sum = (
|
||||
self.transactions.filter(created__gte=month_start, created__lt=month_end).aggregate(total=Sum("amount"))[
|
||||
"total"
|
||||
]
|
||||
self.transactions.filter(
|
||||
created__gte=month_start, created__lt=month_end
|
||||
).aggregate(total=Sum("amount"))["total"]
|
||||
or 0
|
||||
)
|
||||
|
||||
|
|
@ -163,7 +190,9 @@ class Gateway(NiceModel):
|
|||
def can_be_used(self, value: bool):
|
||||
self.__dict__["can_be_used"] = value
|
||||
|
||||
def get_integration_class_object(self, raise_exc: bool = True) -> AbstractGateway | None:
|
||||
def get_integration_class_object(
|
||||
self, raise_exc: bool = True
|
||||
) -> AbstractGateway | None:
|
||||
if not self.integration_path:
|
||||
if raise_exc:
|
||||
raise ValueError(_("gateway integration path is not set"))
|
||||
|
|
@ -171,5 +200,8 @@ class Gateway(NiceModel):
|
|||
try:
|
||||
module_name, class_name = self.integration_path.rsplit(".", 1)
|
||||
except ValueError as exc:
|
||||
raise ValueError(_("invalid integration path: %(path)s") % {"path": self.integration_path}) from exc
|
||||
raise ValueError(
|
||||
_("invalid integration path: %(path)s")
|
||||
% {"path": self.integration_path}
|
||||
) from exc
|
||||
return create_object(module_name, class_name)
|
||||
|
|
|
|||
|
|
@ -16,14 +16,18 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
# noinspection PyUnusedLocal
|
||||
@receiver(post_save, sender=User)
|
||||
def create_balance_on_user_creation_signal(instance: User, created: bool, **kwargs: dict[Any, Any]) -> None:
|
||||
def create_balance_on_user_creation_signal(
|
||||
instance: User, created: bool, **kwargs: dict[Any, Any]
|
||||
) -> None:
|
||||
if created:
|
||||
Balance.objects.create(user=instance)
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@receiver(post_save, sender=Transaction)
|
||||
def process_transaction_changes(instance: Transaction, created: bool, **kwargs: dict[Any, Any]) -> None:
|
||||
def process_transaction_changes(
|
||||
instance: Transaction, created: bool, **kwargs: dict[Any, Any]
|
||||
) -> None:
|
||||
if created:
|
||||
if not instance.gateway:
|
||||
instance.gateway = Gateway.objects.can_be_used().first()
|
||||
|
|
@ -46,7 +50,9 @@ def process_transaction_changes(instance: Transaction, created: bool, **kwargs:
|
|||
)
|
||||
except Exception as e:
|
||||
instance.process = {"status": "ERRORED", "error": str(e)}
|
||||
logger.error(f"Error processing transaction {instance.uuid}: {e}\n{traceback.format_exc()}")
|
||||
logger.error(
|
||||
f"Error processing transaction {instance.uuid}: {e}\n{traceback.format_exc()}"
|
||||
)
|
||||
if not created:
|
||||
status = str(instance.process.get("status", "")).lower()
|
||||
success = instance.process.get("success", False)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ from engine.payments.viewsets import TransactionViewSet
|
|||
app_name = "payments"
|
||||
|
||||
payment_router = DefaultRouter()
|
||||
payment_router.register(prefix=r"transactions", viewset=TransactionViewSet, basename="transactions")
|
||||
payment_router.register(
|
||||
prefix=r"transactions", viewset=TransactionViewSet, basename="transactions"
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path(r"", include(payment_router.urls)),
|
||||
|
|
|
|||
|
|
@ -8,12 +8,16 @@ from django.core.cache import cache
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def update_currencies_to_euro(currency: str, amount: str | float | int | Decimal) -> float:
|
||||
def update_currencies_to_euro(
|
||||
currency: str, amount: str | float | int | Decimal
|
||||
) -> float:
|
||||
logger.warning("update_currencies_to_euro will be deprecated soon!")
|
||||
rates = cache.get("rates", None)
|
||||
|
||||
if not rates:
|
||||
response = requests.get(f"https://rates.icoadm.in/api/v1/rates?key={config.EXCHANGE_RATE_API_KEY}")
|
||||
response = requests.get(
|
||||
f"https://rates.icoadm.in/api/v1/rates?key={config.EXCHANGE_RATE_API_KEY}"
|
||||
)
|
||||
rates = response.json().get("rates")
|
||||
cache.set("rates", rates, 60 * 60 * 24)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ def balance_deposit_email(transaction_pk: str) -> tuple[bool, str]:
|
|||
return False, f"Transaction not found with the given pk: {transaction_pk}"
|
||||
|
||||
if not transaction.balance or not transaction.balance.user:
|
||||
return False, f"Balance not found for the given transaction pk: {transaction_pk}"
|
||||
return (
|
||||
False,
|
||||
f"Balance not found for the given transaction pk: {transaction_pk}",
|
||||
)
|
||||
|
||||
activate(transaction.balance.user.language)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
from typing import Type
|
||||
|
||||
from evibes.utils.misc import create_object
|
||||
from engine.payments.gateways import AbstractGateway
|
||||
from engine.payments.models import Gateway
|
||||
from evibes.utils.misc import create_object
|
||||
|
||||
|
||||
def get_gateways_integrations(name: str | None = None) -> list[Type[AbstractGateway]]:
|
||||
gateways_integrations: list[Type[AbstractGateway]] = []
|
||||
gateways = Gateway.objects.filter(is_active=True, name=name) if name else Gateway.objects.filter(is_active=True)
|
||||
gateways = (
|
||||
Gateway.objects.filter(is_active=True, name=name)
|
||||
if name
|
||||
else Gateway.objects.filter(is_active=True)
|
||||
)
|
||||
for gateway in gateways:
|
||||
if gateway.integration_path:
|
||||
module_name = ".".join(gateway.integration_path.split(".")[:-1])
|
||||
|
|
@ -17,14 +21,17 @@ def get_gateways_integrations(name: str | None = None) -> list[Type[AbstractGate
|
|||
|
||||
|
||||
def get_limits() -> tuple[float, float]:
|
||||
from django.db.models import Min, Max
|
||||
from django.db.models import Max, Min
|
||||
|
||||
qs = Gateway.objects.can_be_used().filter(can_be_used=True)
|
||||
|
||||
if not qs.exists():
|
||||
return 0.0, 0.0
|
||||
|
||||
agg = qs.aggregate(min_limit=Min("minimum_transaction_amount"), max_limit=Max("maximum_transaction_amount"))
|
||||
agg = qs.aggregate(
|
||||
min_limit=Min("minimum_transaction_amount"),
|
||||
max_limit=Max("maximum_transaction_amount"),
|
||||
)
|
||||
|
||||
min_limit = float(agg.get("min_limit") or 0.0)
|
||||
max_limit = float(agg.get("max_limit") or 0.0)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,11 @@ from rest_framework.views import APIView
|
|||
from engine.payments.docs.drf.views import DEPOSIT_SCHEMA, LIMITS_SCHEMA
|
||||
from engine.payments.gateways import UnknownGatewayError
|
||||
from engine.payments.models import Transaction
|
||||
from engine.payments.serializers import DepositSerializer, TransactionProcessSerializer, LimitsSerializer
|
||||
from engine.payments.serializers import (
|
||||
DepositSerializer,
|
||||
LimitsSerializer,
|
||||
TransactionProcessSerializer,
|
||||
)
|
||||
from engine.payments.utils.gateways import get_limits
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -28,7 +32,9 @@ class DepositView(APIView):
|
|||
"with the transaction details is provided."
|
||||
)
|
||||
|
||||
def post(self, request: Request, *args: list[Any], **kwargs: dict[Any, Any]) -> Response:
|
||||
def post(
|
||||
self, request: Request, *args: list[Any], **kwargs: dict[Any, Any]
|
||||
) -> Response:
|
||||
logger.debug(request.__dict__)
|
||||
serializer = DepositSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
|
@ -38,10 +44,15 @@ class DepositView(APIView):
|
|||
|
||||
# noinspection PyUnresolvedReferences
|
||||
transaction = Transaction.objects.create(
|
||||
balance=request.user.payments_balance, amount=serializer.validated_data["amount"], currency="EUR"
|
||||
balance=request.user.payments_balance,
|
||||
amount=serializer.validated_data["amount"],
|
||||
currency="EUR",
|
||||
)
|
||||
|
||||
return Response(TransactionProcessSerializer(transaction).data, status=status.HTTP_303_SEE_OTHER)
|
||||
return Response(
|
||||
TransactionProcessSerializer(transaction).data,
|
||||
status=status.HTTP_303_SEE_OTHER,
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
|
|
@ -54,19 +65,28 @@ class CallbackAPIView(APIView):
|
|||
"indicating success or failure."
|
||||
)
|
||||
|
||||
def post(self, request: Request, *args: list[Any], **kwargs: dict[Any, Any]) -> Response:
|
||||
def post(
|
||||
self, request: Request, *args: list[Any], **kwargs: dict[Any, Any]
|
||||
) -> Response:
|
||||
try:
|
||||
transaction = Transaction.objects.get(uuid=str(kwargs.get("uuid")))
|
||||
if not transaction.gateway:
|
||||
raise UnknownGatewayError(_(f"Transaction {transaction.uuid} has no gateway"))
|
||||
gateway_integration = transaction.gateway.get_integration_class_object(raise_exc=True)
|
||||
raise UnknownGatewayError(
|
||||
_(f"Transaction {transaction.uuid} has no gateway")
|
||||
)
|
||||
gateway_integration = transaction.gateway.get_integration_class_object(
|
||||
raise_exc=True
|
||||
)
|
||||
if not gateway_integration:
|
||||
raise UnknownGatewayError(_(f"Gateway {transaction.gateway} has no integration"))
|
||||
raise UnknownGatewayError(
|
||||
_(f"Gateway {transaction.gateway} has no integration")
|
||||
)
|
||||
gateway_integration.process_callback(request.data)
|
||||
return Response(status=status.HTTP_202_ACCEPTED)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR, data={"error": str(e), "detail": traceback.format_exc()}
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
data={"error": str(e), "detail": traceback.format_exc()},
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -76,7 +96,9 @@ class LimitsAPIView(APIView):
|
|||
"This endpoint returns minimal and maximal allowed deposit amounts across available gateways."
|
||||
)
|
||||
|
||||
def get(self, request: Request, *args: list[Any], **kwargs: dict[Any, Any]) -> Response:
|
||||
def get(
|
||||
self, request: Request, *args: list[Any], **kwargs: dict[Any, Any]
|
||||
) -> Response:
|
||||
min_amount, max_amount = get_limits()
|
||||
data = {"min_amount": min_amount, "max_amount": max_amount}
|
||||
return Response(LimitsSerializer(data).data, status=status.HTTP_200_OK)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ from drf_spectacular.utils import extend_schema_view
|
|||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from engine.core.permissions import EvibesPermission, IsOwner
|
||||
from engine.payments.serializers import TransactionSerializer
|
||||
from engine.payments.docs.drf.viewsets import TRANSACTION_SCHEMA
|
||||
from engine.payments.models import Transaction
|
||||
from engine.payments.serializers import TransactionSerializer
|
||||
|
||||
|
||||
@extend_schema_view(**TRANSACTION_SCHEMA)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from rest_framework_simplejwt.token_blacklist.models import (
|
|||
OutstandingToken as BaseOutstandingToken,
|
||||
)
|
||||
from unfold.admin import ModelAdmin, TabularInline
|
||||
from unfold.forms import AdminPasswordChangeForm, UserCreationForm
|
||||
|
||||
from engine.core.admin import ActivationActionsMixin
|
||||
from engine.core.models import Order
|
||||
|
|
@ -41,7 +42,6 @@ from engine.vibes_auth.models import (
|
|||
ThreadStatus,
|
||||
User,
|
||||
)
|
||||
from unfold.forms import AdminPasswordChangeForm, UserCreationForm
|
||||
|
||||
|
||||
class BalanceInline(TabularInline): # type: ignore [type-arg]
|
||||
|
|
@ -114,17 +114,24 @@ class UserAdmin(ActivationActionsMixin, BaseUserAdmin, ModelAdmin): # type: ign
|
|||
|
||||
def get_queryset(self, request: HttpRequest) -> QuerySet[User]:
|
||||
qs = super().get_queryset(request)
|
||||
return qs.prefetch_related("groups", "payments_balance", "orders").prefetch_related(
|
||||
return qs.prefetch_related(
|
||||
"groups", "payments_balance", "orders"
|
||||
).prefetch_related(
|
||||
Prefetch(
|
||||
"user_permissions",
|
||||
queryset=Permission.objects.select_related("content_type"),
|
||||
)
|
||||
)
|
||||
|
||||
def save_model(self, request: HttpRequest, obj: Any, form: UserForm, change: Any) -> None:
|
||||
def save_model(
|
||||
self, request: HttpRequest, obj: Any, form: UserForm, change: Any
|
||||
) -> None:
|
||||
if form.cleaned_data.get("attributes") is None:
|
||||
obj.attributes = None
|
||||
if form.cleaned_data.get("is_superuser", False) and not request.user.is_superuser:
|
||||
if (
|
||||
form.cleaned_data.get("is_superuser", False)
|
||||
and not request.user.is_superuser
|
||||
):
|
||||
raise PermissionDenied(_("You cannot jump over your head!"))
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ USER_MESSAGE_CONSUMER_SCHEMA = {
|
|||
],
|
||||
"type": "send",
|
||||
"summary": _("User messages entrypoint"),
|
||||
"description": _("Anonymous or authenticated non-staff users send messages. Also supports action=ping."),
|
||||
"description": _(
|
||||
"Anonymous or authenticated non-staff users send messages. Also supports action=ping."
|
||||
),
|
||||
"request": UserMessageRequestSerializer,
|
||||
"responses": UserMessageResponseSerializer,
|
||||
}
|
||||
|
|
@ -26,7 +28,9 @@ STAFF_INBOX_CONSUMER_SCHEMA = {
|
|||
],
|
||||
"type": "send",
|
||||
"summary": _("Staff inbox control"),
|
||||
"description": _("Staff-only actions: list_open, assign, reply, close, ping. Unified event payloads are emitted."),
|
||||
"description": _(
|
||||
"Staff-only actions: list_open, assign, reply, close, ping. Unified event payloads are emitted."
|
||||
),
|
||||
"request": StaffInboxEventSerializer,
|
||||
"responses": StaffInboxEventSerializer,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,11 @@ USER_SCHEMA = {
|
|||
request=ActivateEmailSerializer,
|
||||
responses={
|
||||
status.HTTP_200_OK: UserSerializer,
|
||||
status.HTTP_400_BAD_REQUEST: {"description": _("activation link is invalid or account already activated")},
|
||||
status.HTTP_400_BAD_REQUEST: {
|
||||
"description": _(
|
||||
"activation link is invalid or account already activated"
|
||||
)
|
||||
},
|
||||
**BASE_ERRORS,
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -41,7 +41,12 @@ class CreateUser(BaseMutation):
|
|||
phone_number = String()
|
||||
is_subscribed = Boolean()
|
||||
language = String()
|
||||
referrer = String(required=False, description=_("the user's b64-encoded uuid who referred the new user to us."))
|
||||
referrer = String(
|
||||
required=False,
|
||||
description=_(
|
||||
"the user's b64-encoded uuid who referred the new user to us."
|
||||
),
|
||||
)
|
||||
|
||||
success = Boolean()
|
||||
|
||||
|
|
@ -71,7 +76,9 @@ class CreateUser(BaseMutation):
|
|||
phone_number=phone_number,
|
||||
is_subscribed=is_subscribed if is_subscribed else False,
|
||||
language=language if language else settings.LANGUAGE_CODE,
|
||||
attributes={"referrer": kwargs.get("referrer", "")} if kwargs.get("referrer", "") else {},
|
||||
attributes={"referrer": kwargs.get("referrer", "")}
|
||||
if kwargs.get("referrer", "")
|
||||
else {},
|
||||
)
|
||||
# noinspection PyTypeChecker
|
||||
return CreateUser(success=True)
|
||||
|
|
@ -108,20 +115,28 @@ class UpdateUser(BaseMutation):
|
|||
try:
|
||||
user = User.objects.get(uuid=uuid)
|
||||
|
||||
if not (info.context.user.has_perm("vibes_auth.change_user") or info.context.user == user):
|
||||
if not (
|
||||
info.context.user.has_perm("vibes_auth.change_user")
|
||||
or info.context.user == user
|
||||
):
|
||||
raise PermissionDenied(permission_denied_message)
|
||||
|
||||
email = kwargs.get("email")
|
||||
|
||||
if (email is not None and not is_valid_email(email)) or User.objects.filter(email=email).exclude(
|
||||
uuid=uuid
|
||||
).exists():
|
||||
if (email is not None and not is_valid_email(email)) or User.objects.filter(
|
||||
email=email
|
||||
).exclude(uuid=uuid).exists():
|
||||
raise BadRequest(_("malformed email"))
|
||||
|
||||
phone_number = kwargs.get("phone_number")
|
||||
|
||||
if (phone_number is not None and not is_valid_phone_number(phone_number)) or (
|
||||
User.objects.filter(phone_number=phone_number).exclude(uuid=uuid).exists() and phone_number is not None
|
||||
if (
|
||||
phone_number is not None and not is_valid_phone_number(phone_number)
|
||||
) or (
|
||||
User.objects.filter(phone_number=phone_number)
|
||||
.exclude(uuid=uuid)
|
||||
.exists()
|
||||
and phone_number is not None
|
||||
):
|
||||
raise BadRequest(_(f"malformed phone number: {phone_number}"))
|
||||
|
||||
|
|
@ -131,7 +146,9 @@ class UpdateUser(BaseMutation):
|
|||
if password:
|
||||
validate_password(password=password, user=user)
|
||||
|
||||
if not compare_digest(password, "") and compare_digest(password, confirm_password):
|
||||
if not compare_digest(password, "") and compare_digest(
|
||||
password, confirm_password
|
||||
):
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
|
||||
|
|
@ -145,12 +162,16 @@ class UpdateUser(BaseMutation):
|
|||
user.attributes = {}
|
||||
user.attributes.update({attr: value})
|
||||
else:
|
||||
raise BadRequest(_(f"Invalid attribute format: {attribute_pair}"))
|
||||
raise BadRequest(
|
||||
_(f"Invalid attribute format: {attribute_pair}")
|
||||
)
|
||||
|
||||
for attr, value in kwargs.items():
|
||||
if attr == "password" or attr == "confirm_password":
|
||||
continue
|
||||
if is_safe_key(attr) or info.context.user.has_perm("vibes_auth.change_user"):
|
||||
if is_safe_key(attr) or info.context.user.has_perm(
|
||||
"vibes_auth.change_user"
|
||||
):
|
||||
setattr(user, attr, value)
|
||||
|
||||
user.save()
|
||||
|
|
@ -185,7 +206,9 @@ class DeleteUser(BaseMutation):
|
|||
# noinspection PyTypeChecker
|
||||
return DeleteUser(success=True)
|
||||
except User.DoesNotExist as dne:
|
||||
raise Http404(f"User with the given uuid: {uuid} or email: {email} does not exist.") from dne
|
||||
raise Http404(
|
||||
f"User with the given uuid: {uuid} or email: {email} does not exist."
|
||||
) from dne
|
||||
raise PermissionDenied(permission_denied_message)
|
||||
|
||||
|
||||
|
|
@ -199,7 +222,9 @@ class ObtainJSONWebToken(BaseMutation):
|
|||
access_token = String(required=True)
|
||||
|
||||
def mutate(self, info, email, password):
|
||||
serializer = TokenObtainPairSerializer(data={"email": email, "password": password}, retrieve_user=False)
|
||||
serializer = TokenObtainPairSerializer(
|
||||
data={"email": email, "password": password}, retrieve_user=False
|
||||
)
|
||||
try:
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return ObtainJSONWebToken(
|
||||
|
|
@ -220,7 +245,9 @@ class RefreshJSONWebToken(BaseMutation):
|
|||
refresh_token = String()
|
||||
|
||||
def mutate(self, info, refresh_token):
|
||||
serializer = TokenRefreshSerializer(data={"refresh": refresh_token}, retrieve_user=False)
|
||||
serializer = TokenRefreshSerializer(
|
||||
data={"refresh": refresh_token}, retrieve_user=False
|
||||
)
|
||||
try:
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return RefreshJSONWebToken(
|
||||
|
|
@ -246,7 +273,8 @@ class VerifyJSONWebToken(BaseMutation):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
# noinspection PyTypeChecker
|
||||
return VerifyJSONWebToken(
|
||||
token_is_valid=True, user=User.objects.get(uuid=serializer.validated_data["user"])
|
||||
token_is_valid=True,
|
||||
user=User.objects.get(uuid=serializer.validated_data["user"]),
|
||||
)
|
||||
detail = traceback.format_exc() if settings.DEBUG else ""
|
||||
# noinspection PyTypeChecker
|
||||
|
|
@ -332,7 +360,13 @@ class ConfirmResetPassword(BaseMutation):
|
|||
# noinspection PyTypeChecker
|
||||
return ConfirmResetPassword(success=True)
|
||||
|
||||
except (TypeError, ValueError, OverflowError, ValidationError, User.DoesNotExist) as e:
|
||||
except (
|
||||
TypeError,
|
||||
ValueError,
|
||||
OverflowError,
|
||||
ValidationError,
|
||||
User.DoesNotExist,
|
||||
) as e:
|
||||
raise BadRequest(_(f"something went wrong: {e!s}")) from e
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,12 @@ from graphene.types.generic import GenericScalar
|
|||
from graphene_django import DjangoObjectType
|
||||
from graphql_relay.connection.array_connection import connection_from_array
|
||||
|
||||
from engine.core.graphene.object_types import OrderType, ProductType, WishlistType, AddressType
|
||||
from engine.core.graphene.object_types import (
|
||||
AddressType,
|
||||
OrderType,
|
||||
ProductType,
|
||||
WishlistType,
|
||||
)
|
||||
from engine.core.models import Product, Wishlist
|
||||
from engine.payments.graphene.object_types import BalanceType
|
||||
from engine.payments.models import Balance
|
||||
|
|
@ -37,19 +42,29 @@ class RecentProductConnection(relay.Connection):
|
|||
class UserType(DjangoObjectType):
|
||||
recently_viewed = relay.ConnectionField(
|
||||
RecentProductConnection,
|
||||
description=_("the products this user has viewed most recently (max 48), in reverse‐chronological order"),
|
||||
description=_(
|
||||
"the products this user has viewed most recently (max 48), in reverse‐chronological order"
|
||||
),
|
||||
)
|
||||
groups = List(lambda: GroupType, description=_("groups"))
|
||||
user_permissions = List(lambda: PermissionType, description=_("permissions"))
|
||||
orders = List(lambda: OrderType, description=_("orders"))
|
||||
wishlist = Field(lambda: WishlistType, description=_("wishlist"))
|
||||
balance = Field(lambda: BalanceType, source="payments_balance", description=_("balance"))
|
||||
avatar = String(description=_("avatar"))
|
||||
attributes = GenericScalar(description=_("attributes may be used to store custom data"))
|
||||
language = String(
|
||||
description=_(f"language is one of the {settings.LANGUAGES} with default {settings.LANGUAGE_CODE}")
|
||||
balance = Field(
|
||||
lambda: BalanceType, source="payments_balance", description=_("balance")
|
||||
)
|
||||
avatar = String(description=_("avatar"))
|
||||
attributes = GenericScalar(
|
||||
description=_("attributes may be used to store custom data")
|
||||
)
|
||||
language = String(
|
||||
description=_(
|
||||
f"language is one of the {settings.LANGUAGES} with default {settings.LANGUAGE_CODE}"
|
||||
)
|
||||
)
|
||||
addresses = Field(
|
||||
lambda: AddressType, source="address_set", description=_("address set")
|
||||
)
|
||||
addresses = Field(lambda: AddressType, source="address_set", description=_("address set"))
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
|
@ -123,7 +138,9 @@ class UserType(DjangoObjectType):
|
|||
|
||||
products_by_uuid = {str(p.uuid): p for p in qs}
|
||||
|
||||
ordered_products = [products_by_uuid[u] for u in uuid_list if u in products_by_uuid]
|
||||
ordered_products = [
|
||||
products_by_uuid[u] for u in uuid_list if u in products_by_uuid
|
||||
]
|
||||
|
||||
return connection_from_array(ordered_products, kwargs)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ class UserManager(BaseUserManager):
|
|||
if order.attributes.get("is_business"):
|
||||
mark_business = True
|
||||
if user.phone_number:
|
||||
for order in Order.objects.filter(attributes__icontains=user.phone_number):
|
||||
for order in Order.objects.filter(
|
||||
attributes__icontains=user.phone_number
|
||||
):
|
||||
if not order.user:
|
||||
order.user = user
|
||||
order.save()
|
||||
|
|
@ -80,7 +82,9 @@ class UserManager(BaseUserManager):
|
|||
return user
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def with_perm(self, perm, is_active=True, include_superusers=True, backend=None, obj=None):
|
||||
def with_perm(
|
||||
self, perm, is_active=True, include_superusers=True, backend=None, obj=None
|
||||
):
|
||||
if backend is None:
|
||||
# noinspection PyCallingNonCallable
|
||||
backends = auth._get_backends(return_tuples=True)
|
||||
|
|
@ -92,7 +96,9 @@ class UserManager(BaseUserManager):
|
|||
"therefore must provide the `backend` argument."
|
||||
)
|
||||
elif not isinstance(backend, str):
|
||||
raise TypeError(f"backend must be a dotted import path string (got {backend}).")
|
||||
raise TypeError(
|
||||
f"backend must be a dotted import path string (got {backend})."
|
||||
)
|
||||
else:
|
||||
backend = auth.load_backend(backend)
|
||||
if hasattr(backend, "with_perm"):
|
||||
|
|
|
|||
|
|
@ -36,14 +36,22 @@ def _get_ip(scope) -> str:
|
|||
|
||||
|
||||
async def _is_user_support(user: Any) -> bool:
|
||||
if not getattr(user, "is_authenticated", False) or not getattr(user, "is_staff", False):
|
||||
if not getattr(user, "is_authenticated", False) or not getattr(
|
||||
user, "is_staff", False
|
||||
):
|
||||
return False
|
||||
return await sync_to_async(user.groups.filter(name=USER_SUPPORT_GROUP_NAME).exists)()
|
||||
return await sync_to_async(
|
||||
user.groups.filter(name=USER_SUPPORT_GROUP_NAME).exists
|
||||
)()
|
||||
|
||||
|
||||
async def _get_or_create_ip_thread(ip: str) -> ChatThread:
|
||||
def _inner() -> ChatThread:
|
||||
thread = ChatThread.objects.filter(attributes__ip=ip, status=ThreadStatus.OPEN).order_by("-modified").first()
|
||||
thread = (
|
||||
ChatThread.objects.filter(attributes__ip=ip, status=ThreadStatus.OPEN)
|
||||
.order_by("-modified")
|
||||
.first()
|
||||
)
|
||||
if thread:
|
||||
return thread
|
||||
return ChatThread.objects.create(email="", attributes={"ip": ip})
|
||||
|
|
@ -52,10 +60,18 @@ async def _get_or_create_ip_thread(ip: str) -> ChatThread:
|
|||
|
||||
|
||||
async def _get_or_create_active_thread_for(user: User | None, ip: str) -> ChatThread:
|
||||
if user and getattr(user, "is_authenticated", False) and not getattr(user, "is_staff", False):
|
||||
if (
|
||||
user
|
||||
and getattr(user, "is_authenticated", False)
|
||||
and not getattr(user, "is_staff", False)
|
||||
):
|
||||
|
||||
def _inner_user() -> ChatThread:
|
||||
t = ChatThread.objects.filter(user=user, status=ThreadStatus.OPEN).order_by("-modified").first()
|
||||
t = (
|
||||
ChatThread.objects.filter(user=user, status=ThreadStatus.OPEN)
|
||||
.order_by("-modified")
|
||||
.first()
|
||||
)
|
||||
if t:
|
||||
return t
|
||||
return get_or_create_user_thread(user)
|
||||
|
|
@ -95,11 +111,15 @@ class UserMessageConsumer(AsyncJsonWebsocketConsumer):
|
|||
return
|
||||
|
||||
user: User | None = self.scope.get("user")
|
||||
thread = await _get_or_create_active_thread_for(user if user and user.is_authenticated else None, ip)
|
||||
thread = await _get_or_create_active_thread_for(
|
||||
user if user and user.is_authenticated else None, ip
|
||||
)
|
||||
|
||||
msg = await sync_to_async(send_message)(
|
||||
thread,
|
||||
sender_user=user if user and user.is_authenticated and not user.is_staff else None,
|
||||
sender_user=user
|
||||
if user and user.is_authenticated and not user.is_staff
|
||||
else None,
|
||||
sender_type=SenderType.USER,
|
||||
text=text,
|
||||
)
|
||||
|
|
@ -139,7 +159,9 @@ class StaffInboxConsumer(AsyncJsonWebsocketConsumer):
|
|||
def _list():
|
||||
qs = (
|
||||
ChatThread.objects.filter(status=ThreadStatus.OPEN)
|
||||
.values("uuid", "user_id", "email", "assigned_to_id", "last_message_at")
|
||||
.values(
|
||||
"uuid", "user_id", "email", "assigned_to_id", "last_message_at"
|
||||
)
|
||||
.order_by("-modified")
|
||||
)
|
||||
return list(qs)
|
||||
|
|
@ -160,7 +182,13 @@ class StaffInboxConsumer(AsyncJsonWebsocketConsumer):
|
|||
|
||||
try:
|
||||
t = await sync_to_async(_assign)()
|
||||
await self.send_json({"type": "assigned", "thread_id": str(t.uuid), "user": str(user.uuid)})
|
||||
await self.send_json(
|
||||
{
|
||||
"type": "assigned",
|
||||
"thread_id": str(t.uuid),
|
||||
"user": str(user.uuid),
|
||||
}
|
||||
)
|
||||
except Exception as e: # noqa: BLE001
|
||||
await self.send_json({"error": "assign_failed", "detail": str(e)})
|
||||
return
|
||||
|
|
@ -168,15 +196,25 @@ class StaffInboxConsumer(AsyncJsonWebsocketConsumer):
|
|||
if action == "reply":
|
||||
thread_id = content.get("thread_id")
|
||||
text = content.get("text", "")
|
||||
if not thread_id or not isinstance(text, str) or not (0 < len(text) <= MAX_MESSAGE_LENGTH):
|
||||
if (
|
||||
not thread_id
|
||||
or not isinstance(text, str)
|
||||
or not (0 < len(text) <= MAX_MESSAGE_LENGTH)
|
||||
):
|
||||
await self.send_json({"error": "invalid_payload"})
|
||||
return
|
||||
|
||||
def _can_reply_and_send():
|
||||
thread = ChatThread.objects.get(uuid=thread_id)
|
||||
if thread.assigned_to_id and thread.assigned_to_id != user.id and not user.is_superuser:
|
||||
if (
|
||||
thread.assigned_to_id
|
||||
and thread.assigned_to_id != user.id
|
||||
and not user.is_superuser
|
||||
):
|
||||
raise PermissionError("not_assigned")
|
||||
return send_message(thread, sender_user=user, sender_type=SenderType.STAFF, text=text)
|
||||
return send_message(
|
||||
thread, sender_user=user, sender_type=SenderType.STAFF, text=text
|
||||
)
|
||||
|
||||
try:
|
||||
msg = await sync_to_async(_can_reply_and_send)()
|
||||
|
|
@ -205,16 +243,36 @@ class StaffInboxConsumer(AsyncJsonWebsocketConsumer):
|
|||
await self.send_json({"error": "unknown_action"})
|
||||
|
||||
async def staff_thread_created(self, event):
|
||||
await self.send_json({"type": "staff.thread.created", **{k: v for k, v in event.items() if k != "type"}})
|
||||
await self.send_json(
|
||||
{
|
||||
"type": "staff.thread.created",
|
||||
**{k: v for k, v in event.items() if k != "type"},
|
||||
}
|
||||
)
|
||||
|
||||
async def staff_thread_assigned(self, event):
|
||||
await self.send_json({"type": "staff.thread.assigned", **{k: v for k, v in event.items() if k != "type"}})
|
||||
await self.send_json(
|
||||
{
|
||||
"type": "staff.thread.assigned",
|
||||
**{k: v for k, v in event.items() if k != "type"},
|
||||
}
|
||||
)
|
||||
|
||||
async def staff_thread_reassigned(self, event):
|
||||
await self.send_json({"type": "staff.thread.reassigned", **{k: v for k, v in event.items() if k != "type"}})
|
||||
await self.send_json(
|
||||
{
|
||||
"type": "staff.thread.reassigned",
|
||||
**{k: v for k, v in event.items() if k != "type"},
|
||||
}
|
||||
)
|
||||
|
||||
async def staff_thread_closed(self, event):
|
||||
await self.send_json({"type": "staff.thread.closed", **{k: v for k, v in event.items() if k != "type"}})
|
||||
await self.send_json(
|
||||
{
|
||||
"type": "staff.thread.closed",
|
||||
**{k: v for k, v in event.items() if k != "type"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ThreadConsumer(AsyncJsonWebsocketConsumer):
|
||||
|
|
@ -226,12 +284,16 @@ class ThreadConsumer(AsyncJsonWebsocketConsumer):
|
|||
await self.close(code=4403)
|
||||
return
|
||||
self.thread_id = self.scope["url_route"]["kwargs"].get("thread_id")
|
||||
await self.channel_layer.group_add(f"{THREAD_GROUP_PREFIX}{self.thread_id}", self.channel_name)
|
||||
await self.channel_layer.group_add(
|
||||
f"{THREAD_GROUP_PREFIX}{self.thread_id}", self.channel_name
|
||||
)
|
||||
await self.accept()
|
||||
|
||||
async def disconnect(self, code: int) -> None:
|
||||
if self.thread_id:
|
||||
await self.channel_layer.group_discard(f"{THREAD_GROUP_PREFIX}{self.thread_id}", self.channel_name)
|
||||
await self.channel_layer.group_discard(
|
||||
f"{THREAD_GROUP_PREFIX}{self.thread_id}", self.channel_name
|
||||
)
|
||||
|
||||
@extend_ws_schema(**THREAD_CONSUMER_SCHEMA)
|
||||
async def receive_json(self, content: dict[str, Any], **kwargs) -> None:
|
||||
|
|
@ -239,7 +301,9 @@ class ThreadConsumer(AsyncJsonWebsocketConsumer):
|
|||
user: User = self.scope.get("user")
|
||||
|
||||
if action == "ping":
|
||||
await self.send_json({"type": "pong", "thread": getattr(self, "thread_id", None)})
|
||||
await self.send_json(
|
||||
{"type": "pong", "thread": getattr(self, "thread_id", None)}
|
||||
)
|
||||
return
|
||||
|
||||
if action == "reply":
|
||||
|
|
@ -250,9 +314,15 @@ class ThreadConsumer(AsyncJsonWebsocketConsumer):
|
|||
|
||||
def _reply():
|
||||
thread = ChatThread.objects.get(uuid=self.thread_id)
|
||||
if thread.assigned_to_id and thread.assigned_to_id != user.id and not user.is_superuser:
|
||||
if (
|
||||
thread.assigned_to_id
|
||||
and thread.assigned_to_id != user.id
|
||||
and not user.is_superuser
|
||||
):
|
||||
raise PermissionError("not_assigned")
|
||||
return send_message(thread, sender_user=user, sender_type=SenderType.STAFF, text=text)
|
||||
return send_message(
|
||||
thread, sender_user=user, sender_type=SenderType.STAFF, text=text
|
||||
)
|
||||
|
||||
try:
|
||||
msg = await sync_to_async(_reply)()
|
||||
|
|
@ -277,10 +347,17 @@ class ThreadConsumer(AsyncJsonWebsocketConsumer):
|
|||
await self.send_json({"thread": getattr(self, "thread_id", None), "ok": True})
|
||||
|
||||
async def thread_message(self, event):
|
||||
await self.send_json({"type": "thread.message", **{k: v for k, v in event.items() if k != "type"}})
|
||||
await self.send_json(
|
||||
{
|
||||
"type": "thread.message",
|
||||
**{k: v for k, v in event.items() if k != "type"},
|
||||
}
|
||||
)
|
||||
|
||||
async def thread_closed(self, event):
|
||||
await self.send_json({"type": "thread.closed", **{k: v for k, v in event.items() if k != "type"}})
|
||||
await self.send_json(
|
||||
{"type": "thread.closed", **{k: v for k, v in event.items() if k != "type"}}
|
||||
)
|
||||
|
||||
|
||||
# TODO: Add functionality so non-staff users may audio call staff-user. The call must fall into the queue where
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue