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:
parent
52a62f0f6f
commit
6109643acb
5 changed files with 250 additions and 77 deletions
|
|
@ -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]
|
||||||
|
|
|
||||||
13
engine/vibes_auth/choices.py
Normal file
13
engine/vibes_auth/choices.py
Normal 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")
|
||||||
|
|
@ -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")
|
|
||||||
|
|
@ -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"),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue