2025.4 BETA

This commit is contained in:
Egor Pavlovich Gorbunov 2025-11-10 08:36:57 +03:00
parent c09c0d8753
commit 3fbe6883c7
189 changed files with 3768 additions and 3905 deletions

View file

@ -75,7 +75,7 @@ static/
media/ media/
!engine/core/static !engine/core/static
!engine/blog/static !engine/blog/static
!engine/authv/static !engine/vibes_auth/static
!engine/payments/static !engine/payments/static
# Environment file # Environment file

2
.gitignore vendored
View file

@ -111,7 +111,7 @@ media/
# Allow checked-in static from apps # Allow checked-in static from apps
!engine/core/static/ !engine/core/static/
!engine/payments/static/ !engine/payments/static/
!engine/authv/static/ !engine/vibes_auth/static/
!engine/blog/static/ !engine/blog/static/
# Webassets # Webassets

View file

@ -1,52 +0,0 @@
from __future__ import annotations
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from engine.authv.messaging.models import ChatMessage, ChatThread, ThreadStatus
# noinspection PyUnusedLocal
@admin.register(ChatThread)
class ChatThreadAdmin(admin.ModelAdmin):
list_display = (
"uuid",
"user",
"email",
"assigned_to",
"status",
"last_message_at",
"is_active",
"created",
"modified",
)
list_filter = (
"status",
"is_active",
("assigned_to", admin.EmptyFieldListFilter),
)
search_fields = ("uuid", "email", "user__email", "user__username")
autocomplete_fields = ("user", "assigned_to")
actions = (
"close_threads",
"open_threads",
"delete_selected",
)
readonly_fields = ("created", "modified")
@admin.action(description=_("Close selected threads"))
def close_threads(self, request, queryset): # type: ignore[no-untyped-def]
queryset.update(status=ThreadStatus.CLOSED)
@admin.action(description=_("Open selected threads"))
def open_threads(self, request, queryset): # type: ignore[no-untyped-def]
queryset.update(status=ThreadStatus.OPEN)
@admin.register(ChatMessage)
class ChatMessageAdmin(admin.ModelAdmin):
list_display = ("uuid", "thread", "sender_type", "sender_user", "sent_at")
list_filter = ("sender_type",)
search_fields = ("uuid", "thread__uuid", "sender_user__email")
autocomplete_fields = ("thread", "sender_user")
readonly_fields = ("created", "modified")

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.8 on 2025-04-28 11:56
import uuid import uuid
import django.db.models.deletion import django.db.models.deletion

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.8 on 2025-04-28 12:07
import django_extensions.db.fields import django_extensions.db.fields
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.8 on 2025-04-28 12:39
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-06-29 13:09
import markdown_field.fields import markdown_field.fields
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-09-19 11:59
import markdown_field.fields import markdown_field.fields
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-10-07 12:46
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-10-21 09:24
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,4 @@
import logging import logging
from typing import Optional
import requests import requests
from constance import config from constance import config
@ -165,7 +164,7 @@ class AmoCRM:
def process_order_changes(self, order: Order) -> str: def process_order_changes(self, order: Order) -> str:
with transaction.atomic(): with transaction.atomic():
try: try:
link: Optional[OrderCrmLink] = OrderCrmLink.objects.get(order=order) link: OrderCrmLink | None = OrderCrmLink.objects.get(order=order)
except OrderCrmLink.MultipleObjectsReturned: except OrderCrmLink.MultipleObjectsReturned:
link = OrderCrmLink.objects.filter(order=order).first() link = OrderCrmLink.objects.filter(order=order).first()
except OrderCrmLink.DoesNotExist: except OrderCrmLink.DoesNotExist:

View file

@ -86,8 +86,8 @@ from engine.core.utils import get_project_parameters
from engine.core.utils.languages import get_flag_by_language from engine.core.utils.languages import get_flag_by_language
from engine.core.utils.messages import permission_denied_message from engine.core.utils.messages import permission_denied_message
from engine.payments.graphene.mutations import Deposit from engine.payments.graphene.mutations import Deposit
from engine.authv.filters import UserFilter from engine.vibes_auth.filters import UserFilter
from engine.authv.graphene.mutations import ( from engine.vibes_auth.graphene.mutations import (
ActivateUser, ActivateUser,
ConfirmResetPassword, ConfirmResetPassword,
CreateUser, CreateUser,
@ -99,8 +99,8 @@ from engine.authv.graphene.mutations import (
UploadAvatar, UploadAvatar,
VerifyJSONWebToken, VerifyJSONWebToken,
) )
from engine.authv.graphene.object_types import UserType from engine.vibes_auth.graphene.object_types import UserType
from engine.authv.models import User from engine.vibes_auth.models import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -207,7 +207,7 @@ class Query(ObjectType):
@staticmethod @staticmethod
def resolve_users(_parent, info, **_kwargs): def resolve_users(_parent, info, **_kwargs):
if info.context.user.has_perm("authv.view_user"): if info.context.user.has_perm("vibes_auth.view_user"):
return User.objects.all() return User.objects.all()
users = User.objects.filter(uuid=info.context.user.pk) users = User.objects.filter(uuid=info.context.user.pk)
return users if users.exists() else User.objects.none() return users if users.exists() else User.objects.none()

View file

@ -662,7 +662,7 @@ msgstr ""
"deepl_translate -l en-gb -l ar-ar -l cs-cz -l da-dk -l de-de-de -l en-us -l " "deepl_translate -l en-gb -l ar-ar -l cs-cz -l da-dk -l de-de-de -l en-us -l "
"es-es -l fr-fr -l hi-in -l it-it -l ja-jp -l kk-kz -l nl-nl -l nl-nl -l pl-" "es-es -l fr-fr -l hi-in -l it-it -l ja-jp -l kk-kz -l nl-nl -l nl-nl -l pl-"
"pl -l pt-br -l ro-ro -l ru-ru -l zh-hans -l zh-ans -a core -a geo -a geo -a " "pl -l pt-br -l ro-ro -l ru-ru -l zh-hans -l zh-ans -a core -a geo -a geo -a "
"payments -a authv -a blog" "payments -a vibes_auth -a blog"
#: engine/core/docs/drf/viewsets.py:592 #: engine/core/docs/drf/viewsets.py:592
msgid "limit the results amount, 1 < limit < 10, default: 5" msgid "limit the results amount, 1 < limit < 10, default: 5"

View file

@ -711,7 +711,7 @@ msgstr ""
"docker compose exec app poetry run python manage.py deepl_translate -l en-gb" "docker compose exec app poetry run python manage.py deepl_translate -l en-gb"
" -l ar-ar -l cs-cz -l da-dk -l de-de -l en-us -l es-es -l fr-fr -l hi-in -l " " -l ar-ar -l cs-cz -l da-dk -l de-de -l en-us -l es-es -l fr-fr -l hi-in -l "
"it-it -l ja-jp -l kk-kz -l nl-nl -l pl -l pt-br -l ro-ro -l ru-ru -l zh-hans" "it-it -l ja-jp -l kk-kz -l nl-nl -l pl -l pt-br -l ro-ro -l ru-ru -l zh-hans"
" -a core -a geo -a payments -a authv -a blog" " -a core -a geo -a payments -a vibes_auth -a blog"
#: engine/core/docs/drf/viewsets.py:592 #: engine/core/docs/drf/viewsets.py:592
msgid "limit the results amount, 1 < limit < 10, default: 5" msgid "limit the results amount, 1 < limit < 10, default: 5"

View file

@ -647,7 +647,7 @@ msgstr ""
"docker compose exec app poetry run python manage.py deepl_translate -l en-gb" "docker compose exec app poetry run python manage.py deepl_translate -l en-gb"
" -l ar-ar -l cs-cz -l da-dk -l de-de -l en-us -l es-es -l fr-fr -l hi-in -l " " -l ar-ar -l cs-cz -l da-dk -l de-de -l en-us -l es-es -l fr-fr -l hi-in -l "
"it-it -l ja-jp -l kk-kz -l n-nl -l pl-pl -l pt-br -l ro-ro -l ru-ru -l zh-" "it-it -l ja-jp -l kk-kz -l n-nl -l pl-pl -l pt-br -l ro-ro -l ru-ru -l zh-"
"hans -a core -a geo -a payments -a authv -a blog" "hans -a core -a geo -a payments -a vibes_auth -a blog"
#: engine/core/docs/drf/viewsets.py:592 #: engine/core/docs/drf/viewsets.py:592
msgid "limit the results amount, 1 < limit < 10, default: 5" msgid "limit the results amount, 1 < limit < 10, default: 5"

View file

@ -700,7 +700,7 @@ msgstr ""
"docker compose exec app poetry run python manage.py deepl_translate -l en-gb" "docker compose exec app poetry run python manage.py deepl_translate -l en-gb"
" -l ar-ar -l cs-cz -l da-dk -l de-de -l en-us -l es-es -l fr-fr -l hi-in -l " " -l ar-ar -l cs-cz -l da-dk -l de-de -l en-us -l es-es -l fr-fr -l hi-in -l "
"it-it -l ja-jp -l kk-kz -l nl-nl -l pl-pl -l pt-br -l ro-ro -l ru-ru -l zh-" "it-it -l ja-jp -l kk-kz -l nl-nl -l pl-pl -l pt-br -l ro-ro -l ru-ru -l zh-"
"hans -a core -a geo -a plăți -a authv -a blog" "hans -a core -a geo -a plăți -a vibes_auth -a blog"
#: engine/core/docs/drf/viewsets.py:592 #: engine/core/docs/drf/viewsets.py:592
msgid "limit the results amount, 1 < limit < 10, default: 5" msgid "limit the results amount, 1 < limit < 10, default: 5"

View file

@ -39,4 +39,4 @@ DEEPL_TARGET_LANGUAGES_MAPPING = {
"zh-hans": "ZH-HANS", "zh-hans": "ZH-HANS",
} }
TRANSLATABLE_APPS = ["core", "authv", "blog", "payments", "root"] TRANSLATABLE_APPS = ["core", "vibes_auth", "blog", "payments", "root"]

View file

@ -4,7 +4,7 @@ from typing import Any
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from engine.core.models import Vendor from engine.core.models import Vendor
from engine.authv.models import Group from engine.vibes_auth.models import Group
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.5 on 2025-03-10 11:38
import uuid import uuid
import django.core.validators import django.core.validators

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.5 on 2025-03-10 11:38
import django.contrib.postgres.indexes import django.contrib.postgres.indexes
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.5 on 2025-03-10 12:09
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.5 on 2025-03-10 20:13
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.5 on 2025-03-16 12:53
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.5 on 2025-03-20 15:27
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.5 on 2025-03-24 14:04
from django.db import migrations, models from django.db import migrations, models
import engine.core.validators import engine.core.validators

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.5 on 2025-04-10 15:55
import uuid import uuid
import django.db.models.deletion import django.db.models.deletion

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.5 on 2025-04-15 09:15
import uuid import uuid
import django.db.models.deletion import django.db.models.deletion

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.8 on 2025-04-17 14:22
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.8 on 2025-04-18 11:34
from django.db import migrations, models from django.db import migrations, models
import engine.core.validators import engine.core.validators

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.8 on 2025-04-28 11:56
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.8 on 2025-04-30 13:29
import django_extensions.db.fields import django_extensions.db.fields
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.8 on 2025-04-30 13:42
import django_extensions.db.fields import django_extensions.db.fields
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.8 on 2025-04-30 14:03
import django_extensions.db.fields import django_extensions.db.fields
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.8 on 2025-05-05 12:56
import django_extensions.db.fields import django_extensions.db.fields
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.8 on 2025-05-06 13:58
from django.db import migrations, models from django.db import migrations, models
import engine.core.utils import engine.core.utils

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-05-20 04:57
import uuid import uuid
import django.contrib.gis.db.models.fields import django.contrib.gis.db.models.fields

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-05-20 19:06
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-05-21 09:35
import django_extensions.db.fields import django_extensions.db.fields
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-05-28 19:06
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-06-10 02:42
import uuid import uuid
import django_extensions.db.fields import django_extensions.db.fields

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-06-17 08:25
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-06-18 19:21
import django_extensions.db.fields import django_extensions.db.fields
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-06-20 02:25
import django_extensions.db.fields import django_extensions.db.fields
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-06-21 16:04
import django_extensions.db.fields import django_extensions.db.fields
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-06-21 16:29
import django_extensions.db.fields import django_extensions.db.fields
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-06-21 16:34
import django_extensions.db.fields import django_extensions.db.fields
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-06-21 16:40
import django_extensions.db.fields import django_extensions.db.fields
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-06-21 17:14
import django_extensions.db.fields import django_extensions.db.fields
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-06-21 17:38
from django.db import migrations from django.db import migrations
import engine.core.utils.db import engine.core.utils.db

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-06-21 21:40
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-06-29 13:09
import engine.core.utils.db import engine.core.utils.db
import django_extensions.db.fields import django_extensions.db.fields
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-07-28 08:55
import engine.core.utils import engine.core.utils
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-09-01 17:33
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-09-01 17:36
import engine.core.utils import engine.core.utils
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-09-06 22:05
import django.db.models.deletion import django.db.models.deletion
import django_extensions.db.fields import django_extensions.db.fields
import django_prometheus.models import django_prometheus.models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-09-06 22:16
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-09-19 11:59
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-09-22 11:10
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-10-13 12:15
import engine.core.utils import engine.core.utils
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-10-17 11:27
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-10-18 19:41
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-10-18 21:16
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-10-21 09:24
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2.7 on 2025-10-24 23:17
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2.7 on 2025-10-26 14:10
from django.db import migrations from django.db import migrations

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2.7 on 2025-10-26 16:59
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2.7 on 2025-11-01 23:45
from django.db import migrations, models from django.db import migrations, models

View file

@ -2,7 +2,7 @@ import datetime
import json import json
import logging import logging
from contextlib import suppress from contextlib import suppress
from typing import Any, Optional, Self, Iterable from typing import Any, Iterable, Self
from constance import config from constance import config
from django.conf import settings from django.conf import settings
@ -36,8 +36,8 @@ from django.db.models import (
TextField, TextField,
URLField, URLField,
) )
from django.db.models.indexes import Index
from django.db.models.functions import Length from django.db.models.functions import Length
from django.db.models.indexes import Index
from django.http import Http404 from django.http import Http404
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
@ -64,8 +64,8 @@ from engine.core.utils import (
from engine.core.utils.db import TweakedAutoSlugField, unicode_slugify_function from engine.core.utils.db import TweakedAutoSlugField, unicode_slugify_function
from engine.core.utils.lists import FAILED_STATUSES from engine.core.utils.lists import FAILED_STATUSES
from engine.core.validators import validate_category_image_dimensions from engine.core.validators import validate_category_image_dimensions
from evibes.utils.misc import create_object
from engine.payments.models import Transaction from engine.payments.models import Transaction
from evibes.utils.misc import create_object
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -369,10 +369,10 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel): #
.distinct() .distinct()
) )
per_cat: dict[int, dict[int, dict[str, Any]]] = {} per_cat: dict[Any, Any] = {}
for cat_id, attr_id, attr_name, value_type, value in rows: for cat_id, attr_id, attr_name, value_type, value in rows:
cat_bucket = per_cat.get(cat_id) cat_bucket = per_cat.get(cat_id, "")
if cat_bucket is None: if not cat_bucket:
cat_bucket = {} cat_bucket = {}
per_cat[cat_id] = cat_bucket per_cat[cat_id] = cat_bucket
bucket = cat_bucket.get(attr_id) bucket = cat_bucket.get(attr_id)
@ -405,7 +405,7 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel): #
.distinct() .distinct()
) )
by_attr: dict[int, dict] = {} by_attr: dict[Any, Any] = {}
for attr_id, attr_name, value_type, value in rows: for attr_id, attr_name, value_type, value in rows:
bucket = by_attr.get(attr_id) bucket = by_attr.get(attr_id)
if bucket is None: if bucket is None:
@ -913,7 +913,7 @@ class Wishlist(ExportModelOperationsMixin("wishlist"), NiceModel): # type: igno
verbose_name=_("wishlisted products"), verbose_name=_("wishlisted products"),
) )
user = OneToOneField( user = OneToOneField(
"authv.User", "vibes_auth.User",
on_delete=CASCADE, on_delete=CASCADE,
blank=True, blank=True,
null=True, null=True,
@ -1107,7 +1107,7 @@ class PromoCode(ExportModelOperationsMixin("promocode"), NiceModel): # type: ig
verbose_name=_("usage timestamp"), verbose_name=_("usage timestamp"),
) )
user = ForeignKey( user = ForeignKey(
"authv.User", "vibes_auth.User",
on_delete=CASCADE, on_delete=CASCADE,
help_text=_("user assigned to this promocode if applicable"), help_text=_("user assigned to this promocode if applicable"),
verbose_name=_("assigned user"), verbose_name=_("assigned user"),
@ -1236,7 +1236,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
verbose_name=_("attributes"), verbose_name=_("attributes"),
) )
user = ForeignKey( user = ForeignKey(
"authv.User", "vibes_auth.User",
on_delete=CASCADE, on_delete=CASCADE,
help_text=_("the user who placed the order"), help_text=_("the user who placed the order"),
verbose_name=_("user"), verbose_name=_("user"),
@ -1680,6 +1680,49 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
return None return None
class Feedback(ExportModelOperationsMixin("feedback"), NiceModel): # type: ignore [misc]
__doc__ = _( # type: ignore
"Manages user feedback for products. "
"This class is designed to capture and store user feedback for specific products "
"that they have purchased. It contains attributes to store user comments, "
"a reference to the related product in the order, and a user-assigned rating. The "
"class uses database fields to effectively model and manage feedback data."
)
is_publicly_visible = True
comment = TextField(
blank=True,
null=True,
help_text=_("user-provided comments about their experience with the product"),
verbose_name=_("feedback comments"),
)
order_product = OneToOneField(
"core.OrderProduct",
on_delete=CASCADE,
blank=False,
null=False,
help_text=_("references the specific product in an order that this feedback is about"),
verbose_name=_("related order product"),
)
rating = FloatField(
blank=True,
null=True,
help_text=_("user-assigned rating for the product"),
verbose_name=_("product rating"),
validators=[MinValueValidator(0), MaxValueValidator(10)],
)
def __str__(self) -> str:
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}"
class Meta:
verbose_name = _("feedback")
verbose_name_plural = _("feedbacks")
class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): # type: ignore [misc] class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): # type: ignore [misc]
__doc__ = _( # type: ignore __doc__ = _( # type: ignore
"Represents products associated with orders and their attributes. " "Represents products associated with orders and their attributes. "
@ -1810,7 +1853,7 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): # t
return DigitalAssetDownload.objects.create(order_product=self).url return DigitalAssetDownload.objects.create(order_product=self).url
return "" return ""
def do_feedback(self, rating=10, comment="", action="add") -> Optional["Feedback"] | int: def do_feedback(self, rating=10, comment="", action="add") -> Feedback | int | None:
if not self.order: if not self.order:
raise ValueError(_("order product must have an order")) raise ValueError(_("order product must have an order"))
if action not in ["add", "remove"]: if action not in ["add", "remove"]:
@ -1912,46 +1955,3 @@ class DigitalAssetDownload(ExportModelOperationsMixin("attribute_group"), NiceMo
return ( return (
f"https://api.{config.BASE_DOMAIN}/download/{urlsafe_base64_encode(force_bytes(self.order_product.uuid))}" f"https://api.{config.BASE_DOMAIN}/download/{urlsafe_base64_encode(force_bytes(self.order_product.uuid))}"
) )
class Feedback(ExportModelOperationsMixin("feedback"), NiceModel): # type: ignore [misc]
__doc__ = _( # type: ignore
"Manages user feedback for products. "
"This class is designed to capture and store user feedback for specific products "
"that they have purchased. It contains attributes to store user comments, "
"a reference to the related product in the order, and a user-assigned rating. The "
"class uses database fields to effectively model and manage feedback data."
)
is_publicly_visible = True
comment = TextField(
blank=True,
null=True,
help_text=_("user-provided comments about their experience with the product"),
verbose_name=_("feedback comments"),
)
order_product = OneToOneField(
"core.OrderProduct",
on_delete=CASCADE,
blank=False,
null=False,
help_text=_("references the specific product in an order that this feedback is about"),
verbose_name=_("related order product"),
)
rating = FloatField(
blank=True,
null=True,
help_text=_("user-assigned rating for the product"),
verbose_name=_("product rating"),
validators=[MinValueValidator(0), MaxValueValidator(10)],
)
def __str__(self) -> str:
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}"
class Meta:
verbose_name = _("feedback")
verbose_name_plural = _("feedbacks")

View file

@ -91,7 +91,7 @@ class CategoryDetailSerializer(ModelSerializer):
else: else:
children = obj.children.filter(is_active=True) children = obj.children.filter(is_active=True)
return CategorySimpleSerializer(children, many=True, context=self.context).data if obj.children.exists() else [] return CategorySimpleSerializer(children, many=True, context=self.context).data if obj.children.exists() else [] # type: ignore [return-value]
class BrandDetailSerializer(ModelSerializer): class BrandDetailSerializer(ModelSerializer):

View file

@ -66,7 +66,7 @@ class CategorySimpleSerializer(ModelSerializer): # type: ignore [type-arg]
else: else:
children = obj.children.filter(is_active=True) children = obj.children.filter(is_active=True)
return CategorySimpleSerializer(children, many=True, context=self.context).data if obj.children.exists() else [] 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] class BrandSimpleSerializer(ModelSerializer): # type: ignore [type-arg]

View file

@ -20,7 +20,7 @@ from engine.core.utils import (
) )
from engine.core.utils.emailing import send_order_created_email, send_order_finished_email, send_promocode_created_email 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 evibes.utils.misc import create_object
from engine.authv.models import User from engine.vibes_auth.models import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -5,7 +5,7 @@ import shutil
import uuid import uuid
from datetime import date, timedelta from datetime import date, timedelta
from time import sleep from time import sleep
from typing import Any, Type from typing import Any
import requests import requests
from celery.app import shared_task from celery.app import shared_task
@ -40,10 +40,11 @@ def update_products_task() -> tuple[bool, str]:
if not update_products_task_running: if not update_products_task_running:
cache.set("update_products_task_running", True, 86400) cache.set("update_products_task_running", True, 86400)
vendors: list[Type[AbstractVendor]] = get_vendors_integrations() vendors: list[AbstractVendor] = get_vendors_integrations()
for vendor in vendors: for vendor in vendors:
try: try:
# noinspection PyArgumentList
vendor.update_stock() vendor.update_stock()
except VendorInactiveError: except VendorInactiveError:
logger.info("Skipping %s due to inactivity", str(vendor)) logger.info("Skipping %s due to inactivity", str(vendor))
@ -70,9 +71,10 @@ def update_orderproducts_task() -> tuple[bool, str]:
message confirming the successful execution of the task. message confirming the successful execution of the task.
:rtype: Tuple[bool, str] :rtype: Tuple[bool, str]
""" """
vendors: list[Type[AbstractVendor]] = get_vendors_integrations() vendors: list[AbstractVendor] = get_vendors_integrations()
for vendor in vendors: for vendor in vendors:
# noinspection PyArgumentList
vendor.update_order_products_statuses() vendor.update_order_products_statuses()
return True, "Success" return True, "Success"

View file

@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy as _
from graphene import Context from graphene import Context
from rest_framework.request import Request from rest_framework.request import Request
from engine.authv.models import User from engine.vibes_auth.models import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -1,5 +1,4 @@
import logging import logging
from typing import Type
from engine.core.models import Vendor from engine.core.models import Vendor
from engine.core.vendors import AbstractVendor from engine.core.vendors import AbstractVendor
@ -8,8 +7,8 @@ from evibes.utils.misc import create_object
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_vendors_integrations(name: str | None = None) -> list[Type[AbstractVendor]]: def get_vendors_integrations(name: str | None = None) -> list[AbstractVendor]:
vendors_integrations: list[Type[AbstractVendor]] = [] vendors_integrations: list[AbstractVendor] = []
vendors = Vendor.objects.filter(is_active=True, integration_path__isnull=False) vendors = Vendor.objects.filter(is_active=True, integration_path__isnull=False)
if name: if name:
@ -17,7 +16,7 @@ def get_vendors_integrations(name: str | None = None) -> list[Type[AbstractVendo
for vendor in vendors: for vendor in vendors:
try: try:
module_name, class_name = vendor.integration_path.rsplit(".", 1) module_name, class_name = vendor.integration_path.rsplit(".", 1) # type: ignore [union-attr]
vendors_integrations.append(create_object(module_name, class_name)) vendors_integrations.append(create_object(module_name, class_name))
except Exception as e: except Exception as e:
logger.warning("Couldn't load integration for vendor %s: %s", vendor.name, e) logger.warning("Couldn't load integration for vendor %s: %s", vendor.name, e)

View file

@ -95,7 +95,7 @@ class AbstractVendor:
deletions on inactive objects. deletions on inactive objects.
""" """
def __init__(self, vendor_name: str | None = None, currency: str = "USD") -> None: def __init__(self, vendor_name: str = "", currency: str = "USD") -> None:
self.vendor_name = vendor_name self.vendor_name = vendor_name
self.currency = currency self.currency = currency
self.blocked_attributes: list[Any] = [] self.blocked_attributes: list[Any] = []

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.5 on 2025-03-10 11:38
import uuid import uuid
import django_extensions.db.fields import django_extensions.db.fields

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.5 on 2025-03-10 11:38
import django.contrib.postgres.indexes import django.contrib.postgres.indexes
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.8 on 2025-04-29 11:32
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.1.8 on 2025-04-30 13:29
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2 on 2025-10-21 09:24
import django.db.models.deletion import django.db.models.deletion
import django_extensions.db.fields import django_extensions.db.fields
import uuid import uuid

View file

@ -1,5 +1,3 @@
# Generated by Django 5.2.7 on 2025-10-24 23:17
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,4 @@
from datetime import datetime, time from datetime import datetime, time
from typing import Type
from constance import config from constance import config
from django.conf import settings from django.conf import settings
@ -9,20 +8,20 @@ from django.db.models import (
CharField, CharField,
FloatField, FloatField,
ForeignKey, ForeignKey,
Index,
JSONField, JSONField,
OneToOneField, OneToOneField,
PositiveIntegerField, PositiveIntegerField,
QuerySet, QuerySet,
Sum, Sum,
Index,
) )
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from engine.core.abstract import NiceModel from engine.core.abstract import NiceModel
from evibes.utils.misc import create_object
from engine.payments.gateways import AbstractGateway from engine.payments.gateways import AbstractGateway
from engine.payments.managers import GatewayManager from engine.payments.managers import GatewayManager
from evibes.utils.misc import create_object
class Transaction(NiceModel): class Transaction(NiceModel):
@ -172,7 +171,7 @@ class Gateway(NiceModel):
def can_be_used(self, value: bool): def can_be_used(self, value: bool):
self.__dict__["can_be_used"] = value self.__dict__["can_be_used"] = value
def get_integration_class_object(self, raise_exc: bool = True) -> Type[AbstractGateway] | None: def get_integration_class_object(self, raise_exc: bool = True) -> AbstractGateway | None:
if not self.integration_path: if not self.integration_path:
if raise_exc: if raise_exc:
raise ValueError(_("gateway integration path is not set")) raise ValueError(_("gateway integration path is not set"))

View file

@ -7,7 +7,7 @@ from django.dispatch import receiver
from engine.payments.models import Balance, Transaction, Gateway from engine.payments.models import Balance, Transaction, Gateway
from engine.payments.utils.emailing import balance_deposit_email from engine.payments.utils.emailing import balance_deposit_email
from engine.authv.models import User from engine.vibes_auth.models import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -27,7 +27,7 @@ def process_transaction_changes(instance: Transaction, created: bool, **kwargs:
instance.gateway = Gateway.objects.can_be_used().first() instance.gateway = Gateway.objects.can_be_used().first()
try: try:
gateway = instance.gateway.get_integration_class_object() gateway = instance.gateway.get_integration_class_object()
gateway.process_transaction(instance) gateway.process_transaction(instance) # type: ignore [union-attr]
except Exception as e: except Exception as e:
instance.process = {"status": "ERRORED", "error": str(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()}")

View file

@ -28,8 +28,9 @@ from rest_framework_simplejwt.token_blacklist.models import (
from engine.core.admin import ActivationActionsMixin from engine.core.admin import ActivationActionsMixin
from engine.core.models import Order from engine.core.models import Order
from engine.payments.models import Balance from engine.payments.models import Balance
from engine.authv.forms import UserForm from engine.vibes_auth.forms import UserForm
from engine.authv.models import BlacklistedToken, Group, OutstandingToken, User from engine.vibes_auth.messaging.models import ChatMessage, ChatThread, ThreadStatus
from engine.vibes_auth.models import BlacklistedToken, Group, OutstandingToken, User
class BalanceInline(admin.TabularInline): # type: ignore [type-arg] class BalanceInline(admin.TabularInline): # type: ignore [type-arg]
@ -113,6 +114,52 @@ class UserAdmin(ActivationActionsMixin, BaseUserAdmin): # type: ignore [misc, t
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
# noinspection PyUnusedLocal
@admin.register(ChatThread)
class ChatThreadAdmin(admin.ModelAdmin):
list_display = (
"uuid",
"user",
"email",
"assigned_to",
"status",
"last_message_at",
"is_active",
"created",
"modified",
)
list_filter = (
"status",
"is_active",
("assigned_to", admin.EmptyFieldListFilter),
)
search_fields = ("uuid", "email", "user__email", "user__username")
autocomplete_fields = ("user", "assigned_to")
actions = (
"close_threads",
"open_threads",
"delete_selected",
)
readonly_fields = ("created", "modified")
@admin.action(description=_("Close selected threads"))
def close_threads(self, request, queryset): # type: ignore[no-untyped-def]
queryset.update(status=ThreadStatus.CLOSED)
@admin.action(description=_("Open selected threads"))
def open_threads(self, request, queryset): # type: ignore[no-untyped-def]
queryset.update(status=ThreadStatus.OPEN)
@admin.register(ChatMessage)
class ChatMessageAdmin(admin.ModelAdmin):
list_display = ("uuid", "thread", "sender_type", "sender_user", "sent_at")
list_filter = ("sender_type",)
search_fields = ("uuid", "thread__uuid", "sender_user__email")
autocomplete_fields = ("thread", "sender_user")
readonly_fields = ("created", "modified")
class GroupAdmin(BaseGroupAdmin): class GroupAdmin(BaseGroupAdmin):
pass pass

View file

@ -4,11 +4,11 @@ from django.utils.translation import gettext_lazy as _
class VibesAuthConfig(AppConfig): class VibesAuthConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"
name = "engine.authv" name = "engine.vibes_auth"
verbose_name = _("authentication") verbose_name = _("authentication")
icon = "fa fa-solid fa-user" icon = "fa fa-solid fa-user"
priority = 89 priority = 89
hide = False hide = False
def ready(self) -> None: def ready(self) -> None:
import engine.authv.signals # noqa: F401 import engine.vibes_auth.signals # noqa: F401

View file

@ -3,7 +3,7 @@ from drf_spectacular.utils import extend_schema, inline_serializer
from rest_framework import serializers, status from rest_framework import serializers, status
from engine.core.docs.drf import error from engine.core.docs.drf import error
from engine.authv.serializers import ( from engine.vibes_auth.serializers import (
TokenObtainPairSerializer, TokenObtainPairSerializer,
TokenRefreshSerializer, TokenRefreshSerializer,
TokenVerifySerializer, TokenVerifySerializer,

View file

@ -3,7 +3,7 @@ from drf_spectacular.utils import extend_schema
from rest_framework import status from rest_framework import status
from engine.core.docs.drf import BASE_ERRORS from engine.core.docs.drf import BASE_ERRORS
from engine.authv.serializers import ( from engine.vibes_auth.serializers import (
ActivateEmailSerializer, ActivateEmailSerializer,
ConfirmPasswordResetSerializer, ConfirmPasswordResetSerializer,
MergeRecentlyViewedSerializer, MergeRecentlyViewedSerializer,

View file

@ -1,6 +1,6 @@
import django_filters import django_filters
from engine.authv.models import User from engine.vibes_auth.models import User
class UserFilter(django_filters.FilterSet): class UserFilter(django_filters.FilterSet):

View file

@ -1,7 +1,7 @@
from django.forms import ModelForm from django.forms import ModelForm
from engine.core.widgets import JSONTableWidget from engine.core.widgets import JSONTableWidget
from engine.authv.models import User from engine.vibes_auth.models import User
class UserForm(ModelForm): # type: ignore [type-arg] class UserForm(ModelForm): # type: ignore [type-arg]

View file

@ -15,15 +15,15 @@ from graphene_file_upload.scalars import Upload
from engine.core.graphene import BaseMutation from engine.core.graphene import BaseMutation
from engine.core.utils.messages import permission_denied_message from engine.core.utils.messages import permission_denied_message
from engine.authv.graphene.object_types import UserType from engine.vibes_auth.graphene.object_types import UserType
from engine.authv.models import User from engine.vibes_auth.models import User
from engine.authv.serializers import ( from engine.vibes_auth.serializers import (
TokenObtainPairSerializer, TokenObtainPairSerializer,
TokenRefreshSerializer, TokenRefreshSerializer,
TokenVerifySerializer, TokenVerifySerializer,
) )
from engine.authv.utils.emailing import send_reset_password_email_task from engine.vibes_auth.utils.emailing import send_reset_password_email_task
from engine.authv.validators import is_valid_email, is_valid_phone_number from engine.vibes_auth.validators import is_valid_email, is_valid_phone_number
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -109,7 +109,7 @@ class UpdateUser(BaseMutation):
name = "User" name = "User"
raise Http404(_(f"{name} does not exist: {uuid}")) from dne raise Http404(_(f"{name} does not exist: {uuid}")) from dne
if not (info.context.user.has_perm("authv.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) raise PermissionDenied(permission_denied_message)
email = kwargs.get("email") email = kwargs.get("email")
@ -158,7 +158,7 @@ class UpdateUser(BaseMutation):
"is_staff", "is_staff",
"is_active", "is_active",
"is_superuser", "is_superuser",
] or info.context.user.has_perm("authv.change_user"): ] or info.context.user.has_perm("vibes_auth.change_user"):
setattr(user, attr, value) setattr(user, attr, value)
user.save() user.save()
@ -174,7 +174,7 @@ class DeleteUser(BaseMutation):
success = Boolean() success = Boolean()
def mutate(self, info, uuid=None, email=None): def mutate(self, info, uuid=None, email=None):
if info.context.user.has_perm("authv.delete_user"): if info.context.user.has_perm("vibes_auth.delete_user"):
try: try:
if uuid is not None: if uuid is not None:
User.objects.get(uuid=uuid).delete() User.objects.get(uuid=uuid).delete()

View file

@ -10,7 +10,7 @@ from engine.core.graphene.object_types import OrderType, ProductType, WishlistTy
from engine.core.models import Product, Wishlist from engine.core.models import Product, Wishlist
from engine.payments.graphene.object_types import BalanceType from engine.payments.graphene.object_types import BalanceType
from engine.payments.models import Balance from engine.payments.models import Balance
from engine.authv.models import User from engine.vibes_auth.models import User
class GroupType(DjangoObjectType): class GroupType(DjangoObjectType):

Some files were not shown because too many files have changed in this diff Show more