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.core.models import Order
from engine.payments.models import Balance from engine.payments.models import Balance
from engine.vibes_auth.forms import UserForm from engine.vibes_auth.forms import UserForm
from engine.vibes_auth.messaging.models import ChatMessage, ChatThread, ThreadStatus from engine.vibes_auth.models import (
from engine.vibes_auth.models import BlacklistedToken, Group, OutstandingToken, User BlacklistedToken,
Group,
OutstandingToken,
User,
ChatMessage,
ChatThread,
ThreadStatus,
)
class BalanceInline(admin.TabularInline): # type: ignore [type-arg] 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 AbstractUser
from django.contrib.auth.models import Group as BaseGroup from django.contrib.auth.models import Group as BaseGroup
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db.models import ( from django.db.models import (
BooleanField, BooleanField,
CharField, CharField,
@ -11,7 +12,14 @@ from django.db.models import (
ImageField, ImageField,
JSONField, JSONField,
UUIDField, UUIDField,
ForeignKey,
TextField,
DateTimeField,
Index,
CASCADE,
SET_NULL,
) )
from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework_simplejwt.token_blacklist.models import ( from rest_framework_simplejwt.token_blacklist.models import (
BlacklistedToken as BaseBlacklistedToken, BlacklistedToken as BaseBlacklistedToken,
@ -21,6 +29,7 @@ from rest_framework_simplejwt.token_blacklist.models import (
) )
from engine.core.abstract import NiceModel 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.managers import UserManager
from engine.vibes_auth.validators import validate_phone_number from engine.vibes_auth.validators import validate_phone_number
from engine.payments.models import Balance from engine.payments.models import Balance
@ -116,6 +125,50 @@ class User(AbstractUser, NiceModel): # type: ignore [django-manager-missing]
verbose_name_plural = _("users") 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 Group(BaseGroup):
class Meta: class Meta:
proxy = True proxy = True