From 6109643acbb56335cbe6dd7b6bf8322a8efeef58 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Tue, 11 Nov 2025 18:31:17 +0300 Subject: [PATCH] Features: 1) Introduced `ChatThread` and `ChatMessage` models for messaging functionality; 2) Added `ThreadStatus` and `SenderType` choices for structured messaging states; 3) Integrated Django migrations with indexing for new models; Fixes: 1) Corrected `admin.py` imports for consistency and model alignment; Extra: Refactored `choices.py` for reusable enums; restructured `models.py` for clarity and maintainability. --- engine/vibes_auth/admin.py | 11 +- engine/vibes_auth/choices.py | 13 ++ engine/vibes_auth/messaging/models.py | 75 -------- .../0006_chatthread_chatmessage_and_more.py | 175 ++++++++++++++++++ engine/vibes_auth/models.py | 53 ++++++ 5 files changed, 250 insertions(+), 77 deletions(-) create mode 100644 engine/vibes_auth/choices.py delete mode 100644 engine/vibes_auth/messaging/models.py create mode 100644 engine/vibes_auth/migrations/0006_chatthread_chatmessage_and_more.py diff --git a/engine/vibes_auth/admin.py b/engine/vibes_auth/admin.py index e97810e2..a5bceaaa 100644 --- a/engine/vibes_auth/admin.py +++ b/engine/vibes_auth/admin.py @@ -29,8 +29,15 @@ from engine.core.admin import ActivationActionsMixin from engine.core.models import Order from engine.payments.models import Balance from engine.vibes_auth.forms import UserForm -from engine.vibes_auth.messaging.models import ChatMessage, ChatThread, ThreadStatus -from engine.vibes_auth.models import BlacklistedToken, Group, OutstandingToken, User +from engine.vibes_auth.models import ( + BlacklistedToken, + Group, + OutstandingToken, + User, + ChatMessage, + ChatThread, + ThreadStatus, +) class BalanceInline(admin.TabularInline): # type: ignore [type-arg] diff --git a/engine/vibes_auth/choices.py b/engine/vibes_auth/choices.py new file mode 100644 index 00000000..72a28b87 --- /dev/null +++ b/engine/vibes_auth/choices.py @@ -0,0 +1,13 @@ +from django.db.models import TextChoices +from django.utils.translation import gettext_lazy as _ + + +class ThreadStatus(TextChoices): + OPEN = "open", _("Open") + CLOSED = "closed", _("Closed") + + +class SenderType(TextChoices): + USER = "user", _("User") + STAFF = "staff", _("Staff") + SYSTEM = "system", _("System") diff --git a/engine/vibes_auth/messaging/models.py b/engine/vibes_auth/messaging/models.py deleted file mode 100644 index 12973ea3..00000000 --- a/engine/vibes_auth/messaging/models.py +++ /dev/null @@ -1,75 +0,0 @@ -from __future__ import annotations - -from django.core.exceptions import ValidationError -from django.db.models import ( - CharField, - DateTimeField, - EmailField, - ForeignKey, - Index, - JSONField, - TextChoices, - TextField, - SET_NULL, - CASCADE, -) -from django.utils import timezone -from django.utils.translation import gettext_lazy as _ - -from engine.core.abstract import NiceModel -from engine.vibes_auth.models import User - - -class ThreadStatus(TextChoices): - OPEN = "open", _("Open") - CLOSED = "closed", _("Closed") - - -class SenderType(TextChoices): - USER = "user", _("User") - STAFF = "staff", _("Staff") - SYSTEM = "system", _("System") - - -class ChatThread(NiceModel): - user = ForeignKey(User, null=True, blank=True, on_delete=SET_NULL, related_name="chat_threads") - email = EmailField(blank=True, default="", help_text=_("For anonymous threads")) - assigned_to = ForeignKey(User, null=True, blank=True, on_delete=SET_NULL, related_name="assigned_chat_threads") - status = CharField(max_length=16, choices=ThreadStatus.choices, default=ThreadStatus.OPEN) - last_message_at = DateTimeField(default=timezone.now) - attributes = JSONField(default=dict, blank=True) - - class Meta: - indexes = [ - Index(fields=["status", "modified"], name="chatthread_status_mod_idx"), - Index(fields=["assigned_to", "status"], name="chatthread_assigned_status_idx"), - Index(fields=["user"], name="chatthread_user_idx"), - Index(fields=["email"], name="chatthread_email_idx"), - ] - ordering = ("-modified",) - verbose_name = _("Chat thread") - verbose_name_plural = _("Chat threads") - - def clean(self) -> None: - super().clean() - if not self.user and not self.email: - raise ValidationError({"email": _("Provide user or email for anonymous thread.")}) - if self.assigned_to and not self.assigned_to.is_staff: - raise ValidationError({"assigned_to": _("Assignee must be a staff user.")}) - - -class ChatMessage(NiceModel): - thread = ForeignKey(ChatThread, on_delete=CASCADE, related_name="messages") - sender_type = CharField(max_length=16, choices=SenderType.choices) - sender_user = ForeignKey(User, null=True, blank=True, on_delete=SET_NULL, related_name="chat_messages") - text = TextField() - sent_at = DateTimeField(default=timezone.now) - attributes = JSONField(default=dict, blank=True) - - class Meta: - indexes = [ - Index(fields=["thread", "sent_at"], name="chatmessage_thread_sent_idx"), - ] - ordering = ("sent_at", "uuid") - verbose_name = _("Chat message") - verbose_name_plural = _("Chat messages") diff --git a/engine/vibes_auth/migrations/0006_chatthread_chatmessage_and_more.py b/engine/vibes_auth/migrations/0006_chatthread_chatmessage_and_more.py new file mode 100644 index 00000000..c5d3adb2 --- /dev/null +++ b/engine/vibes_auth/migrations/0006_chatthread_chatmessage_and_more.py @@ -0,0 +1,175 @@ +# Generated by Django 5.2.8 on 2025-11-11 15:28 + +import django.db.models.deletion +import django.utils.timezone +import django_extensions.db.fields +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("vibes_auth", "0005_alter_user_groups"), + ] + + operations = [ + migrations.CreateModel( + name="ChatThread", + fields=[ + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + help_text="unique id is used to surely identify any database object", + primary_key=True, + serialize=False, + verbose_name="unique id", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="if set to false, this object can't be seen by users without needed permission", + verbose_name="is active", + ), + ), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, + help_text="when the object first appeared on the database", + verbose_name="created", + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, help_text="when the object was last modified", verbose_name="modified" + ), + ), + ("email", models.EmailField(blank=True, default="", help_text="For anonymous threads", max_length=254)), + ( + "status", + models.CharField(choices=[("open", "Open"), ("closed", "Closed")], default="open", max_length=16), + ), + ("last_message_at", models.DateTimeField(default=django.utils.timezone.now)), + ("attributes", models.JSONField(blank=True, default=dict)), + ( + "assigned_to", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="assigned_chat_threads", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="chat_threads", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "Chat thread", + "verbose_name_plural": "Chat threads", + "ordering": ("-modified",), + }, + ), + migrations.CreateModel( + name="ChatMessage", + fields=[ + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + help_text="unique id is used to surely identify any database object", + primary_key=True, + serialize=False, + verbose_name="unique id", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="if set to false, this object can't be seen by users without needed permission", + verbose_name="is active", + ), + ), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, + help_text="when the object first appeared on the database", + verbose_name="created", + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, help_text="when the object was last modified", verbose_name="modified" + ), + ), + ( + "sender_type", + models.CharField( + choices=[("user", "User"), ("staff", "Staff"), ("system", "System")], max_length=16 + ), + ), + ("text", models.TextField()), + ("sent_at", models.DateTimeField(default=django.utils.timezone.now)), + ("attributes", models.JSONField(blank=True, default=dict)), + ( + "sender_user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="chat_messages", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "thread", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="messages", to="vibes_auth.chatthread" + ), + ), + ], + options={ + "verbose_name": "Chat message", + "verbose_name_plural": "Chat messages", + "ordering": ("sent_at", "uuid"), + }, + ), + migrations.AddIndex( + model_name="chatthread", + index=models.Index(fields=["status", "modified"], name="chatthread_status_mod_idx"), + ), + migrations.AddIndex( + model_name="chatthread", + index=models.Index(fields=["assigned_to", "status"], name="chatthread_assigned_status_idx"), + ), + migrations.AddIndex( + model_name="chatthread", + index=models.Index(fields=["user"], name="chatthread_user_idx"), + ), + migrations.AddIndex( + model_name="chatthread", + index=models.Index(fields=["email"], name="chatthread_email_idx"), + ), + migrations.AddIndex( + model_name="chatmessage", + index=models.Index(fields=["thread", "sent_at"], name="chatmessage_thread_sent_idx"), + ), + ] diff --git a/engine/vibes_auth/models.py b/engine/vibes_auth/models.py index 9622145a..c5ae191c 100644 --- a/engine/vibes_auth/models.py +++ b/engine/vibes_auth/models.py @@ -4,6 +4,7 @@ from django.conf import settings from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import Group as BaseGroup from django.core.cache import cache +from django.core.exceptions import ValidationError from django.db.models import ( BooleanField, CharField, @@ -11,7 +12,14 @@ from django.db.models import ( ImageField, JSONField, UUIDField, + ForeignKey, + TextField, + DateTimeField, + Index, + CASCADE, + SET_NULL, ) +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from rest_framework_simplejwt.token_blacklist.models import ( BlacklistedToken as BaseBlacklistedToken, @@ -21,6 +29,7 @@ from rest_framework_simplejwt.token_blacklist.models import ( ) from engine.core.abstract import NiceModel +from engine.vibes_auth.choices import SenderType, ThreadStatus from engine.vibes_auth.managers import UserManager from engine.vibes_auth.validators import validate_phone_number from engine.payments.models import Balance @@ -116,6 +125,50 @@ class User(AbstractUser, NiceModel): # type: ignore [django-manager-missing] verbose_name_plural = _("users") +class ChatThread(NiceModel): + user = ForeignKey(User, null=True, blank=True, on_delete=SET_NULL, related_name="chat_threads") + email = EmailField(blank=True, default="", help_text=_("For anonymous threads")) + assigned_to = ForeignKey(User, null=True, blank=True, on_delete=SET_NULL, related_name="assigned_chat_threads") + status = CharField(max_length=16, choices=ThreadStatus.choices, default=ThreadStatus.OPEN) + last_message_at = DateTimeField(default=timezone.now) + attributes = JSONField(default=dict, blank=True) + + class Meta: + indexes = [ + Index(fields=["status", "modified"], name="chatthread_status_mod_idx"), + Index(fields=["assigned_to", "status"], name="chatthread_assigned_status_idx"), + Index(fields=["user"], name="chatthread_user_idx"), + Index(fields=["email"], name="chatthread_email_idx"), + ] + ordering = ("-modified",) + verbose_name = _("Chat thread") + verbose_name_plural = _("Chat threads") + + def clean(self) -> None: + super().clean() + if not self.user and not self.email: + raise ValidationError({"email": _("provide user or email for anonymous thread.")}) + if self.assigned_to and not self.assigned_to.is_staff: + raise ValidationError({"assigned_to": _("assignee must be a staff user.")}) + + +class ChatMessage(NiceModel): + thread = ForeignKey(ChatThread, on_delete=CASCADE, related_name="messages") + sender_type = CharField(max_length=16, choices=SenderType.choices) + sender_user = ForeignKey(User, null=True, blank=True, on_delete=SET_NULL, related_name="chat_messages") + text = TextField() + sent_at = DateTimeField(default=timezone.now) + attributes = JSONField(default=dict, blank=True) + + class Meta: + indexes = [ + Index(fields=["thread", "sent_at"], name="chatmessage_thread_sent_idx"), + ] + ordering = ("sent_at", "uuid") + verbose_name = _("Chat message") + verbose_name_plural = _("Chat messages") + + class Group(BaseGroup): class Meta: proxy = True