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.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-11-11 18:31:17 +03:00
parent 52a62f0f6f
commit 6109643acb
5 changed files with 250 additions and 77 deletions

View file

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

View file

@ -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")

View file

@ -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")

View file

@ -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"),
),
]

View file

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