This commit is contained in:
javad committed 2026-09-10 11:34:19 +03:30
commit fa9d154db9
260 files changed
+17643

No files matched your search

View File
Whitespace-only changes.
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
from .models import Message,MessageMention,MessageRevision
admin.site.register([Message,MessageMention,MessageRevision])
+2
View File
@@ -0,0 +1,2 @@
from django.apps import AppConfig
class MessagingConfig(AppConfig): name="apps.messaging"
+53
View File
@@ -0,0 +1,53 @@
# Generated by Django 5.1.1 on 2026-09-05 07:03
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='MessageMention',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='MessageRevision',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('body', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='Message',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('public_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('body', models.TextField(blank=True)),
('message_type', models.CharField(choices=[('PUBLIC', 'عمومی'), ('INTERNAL_NOTE', 'یادداشت داخلی')], default='PUBLIC', max_length=20)),
('is_internal', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('edited_at', models.DateTimeField(blank=True, null=True)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='authored_messages', to=settings.AUTH_USER_MODEL)),
('deleted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='deleted_messages', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['created_at', 'id'],
'permissions': [('add_internal_note', 'Can add internal note'), ('edit_own_message', 'Can edit own message'), ('delete_own_message', 'Can delete own message'), ('view_deleted_message_content', 'Can view deleted content'), ('view_message_revisions', 'Can view revisions')],
},
),
]
+48
View File
@@ -0,0 +1,48 @@
# Generated by Django 5.1.1 on 2026-09-05 07:03
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('messaging', '0001_initial'),
('tickets', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='message',
name='ticket',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='ticket_messages', to='tickets.ticket'),
),
migrations.AddField(
model_name='messagemention',
name='mentioned_user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='messagemention',
name='message',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mentions', to='messaging.message'),
),
migrations.AddField(
model_name='messagerevision',
name='edited_by',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='messagerevision',
name='message',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='messaging.message'),
),
migrations.AddConstraint(
model_name='messagemention',
constraint=models.UniqueConstraint(fields=('message', 'mentioned_user'), name='unique_message_mention'),
),
]
@@ -0,0 +1,29 @@
# Generated by Django 5.1.1 on 2026-09-06 15:45
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('messaging', '0002_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='MessageReadReceipt',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('read_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='read_receipts', to='messaging.message')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='message_read_receipts', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['read_at', 'id'],
'constraints': [models.UniqueConstraint(fields=('message', 'user'), name='unique_message_read_receipt')],
},
),
]
Whitespace-only changes.
+31
View File
@@ -0,0 +1,31 @@
from django.conf import settings
from django.db import models
from apps.core.models import PersianTextNormalizationMixin, PublicIdModel
class Message(PersianTextNormalizationMixin, PublicIdModel, models.Model):
persian_text_fields = {"body": True}
class Type(models.TextChoices): PUBLIC="PUBLIC","عمومی"; INTERNAL_NOTE="INTERNAL_NOTE","یادداشت داخلی"
ticket=models.ForeignKey("tickets.Ticket",on_delete=models.PROTECT,related_name="ticket_messages"); author=models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.PROTECT,related_name="authored_messages"); body=models.TextField(blank=True); message_type=models.CharField(max_length=20,choices=Type.choices,default=Type.PUBLIC); is_internal=models.BooleanField(default=False); created_at=models.DateTimeField(auto_now_add=True,db_index=True); updated_at=models.DateTimeField(auto_now=True); edited_at=models.DateTimeField(null=True,blank=True); deleted_at=models.DateTimeField(null=True,blank=True); deleted_by=models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.PROTECT,null=True,blank=True,related_name="deleted_messages")
class Meta:
ordering=["created_at","id"]
permissions=[("add_internal_note","Can add internal note"),("edit_own_message","Can edit own message"),("delete_own_message","Can delete own message"),("view_deleted_message_content","Can view deleted content"),("view_message_revisions","Can view revisions")]
@property
def author_name(self):
profile=getattr(self.author,"staff_profile",None) or getattr(self.author,"customer_profile",None)
return str(profile) if profile else self.author.username
class MessageRevision(PersianTextNormalizationMixin, models.Model):
persian_text_fields = {"body": True}
message=models.ForeignKey(Message,on_delete=models.CASCADE,related_name="revisions"); body=models.TextField(); edited_by=models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.PROTECT); created_at=models.DateTimeField(auto_now_add=True)
class MessageMention(models.Model):
message=models.ForeignKey(Message,on_delete=models.CASCADE,related_name="mentions"); mentioned_user=models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE); created_at=models.DateTimeField(auto_now_add=True)
class Meta: constraints=[models.UniqueConstraint(fields=["message","mentioned_user"],name="unique_message_mention")]
class MessageReadReceipt(models.Model):
message=models.ForeignKey(Message,on_delete=models.CASCADE,related_name="read_receipts")
user=models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE,related_name="message_read_receipts")
read_at=models.DateTimeField(auto_now_add=True,db_index=True)
class Meta:
ordering=["read_at","id"]
constraints=[models.UniqueConstraint(fields=["message","user"],name="unique_message_read_receipt")]
@property
def reader_name(self):
profile=getattr(self.user,"staff_profile",None) or getattr(self.user,"customer_profile",None)
return str(profile) if profile else self.user.username
+21
View File
@@ -0,0 +1,21 @@
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.utils import timezone
from apps.core.persian_text import normalize_persian_text
from apps.tickets.policies import TicketPolicy
from .models import Message,MessageRevision
@transaction.atomic
def edit_message(message_id,user,body):
msg=Message.objects.select_for_update().select_related("ticket").get(pk=message_id)
if msg.author_id!=user.id or not TicketPolicy.can_view(user,msg.ticket): raise PermissionDenied
if not (user.account_type=="CUSTOMER" or user.has_perm("messaging.edit_own_message")): raise PermissionDenied
if msg.deleted_at: raise PermissionDenied
body=normalize_persian_text(body,multiline=True)
if not body: raise ValueError("متن پیام نمی‌تواند خالی باشد.")
MessageRevision.objects.create(message=msg,body=msg.body,edited_by=user); msg.body=body; msg.edited_at=timezone.now(); msg.save(update_fields=["body","edited_at","updated_at"]); return msg
@transaction.atomic
def soft_delete_message(message_id,user):
msg=Message.objects.select_for_update().select_related("ticket").get(pk=message_id)
may_delete=user.account_type=="CUSTOMER" or user.has_perm("messaging.delete_own_message")
if msg.author_id!=user.id or not may_delete or not TicketPolicy.can_view(user,msg.ticket): raise PermissionDenied
msg.deleted_at=timezone.now(); msg.deleted_by=user; msg.save(update_fields=["deleted_at","deleted_by","updated_at"]); return msg
+5
View File
@@ -0,0 +1,5 @@
from django.urls import path
from . import views
app_name="messages"
urlpatterns=[path("<uuid:public_id>/edit/",views.edit,name="edit"),path("<uuid:public_id>/delete/",views.delete,name="delete"),path("<uuid:public_id>/revisions/",views.revisions,name="revisions")]
+28
View File
@@ -0,0 +1,28 @@
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.http import JsonResponse
from django.shortcuts import get_object_or_404,redirect,render
from django.utils.http import url_has_allowed_host_and_scheme
from django.views.decorators.http import require_POST
from .models import Message
from .services import edit_message,soft_delete_message
from apps.tickets.policies import TicketPolicy
def _safe_next(request, fallback):
value=request.POST.get("next")
return value if value and url_has_allowed_host_and_scheme(value,allowed_hosts={request.get_host()},require_https=request.is_secure()) else fallback
@login_required
@require_POST
def edit(request,public_id):
msg=get_object_or_404(Message,public_id=public_id)
try: edit_message(msg.pk,request.user,request.POST.get("body",""))
except (PermissionDenied,ValueError) as exc: raise PermissionDenied(str(exc))
return redirect(_safe_next(request,"/tickets/"))
@login_required
@require_POST
def delete(request,public_id):
msg=get_object_or_404(Message,public_id=public_id); soft_delete_message(msg.pk,request.user); return redirect(_safe_next(request,"/staff/"))
@login_required
def revisions(request,public_id):
msg=get_object_or_404(Message,public_id=public_id)
if not request.user.has_perm("messaging.view_message_revisions") or not TicketPolicy.can_view(request.user,msg.ticket): raise PermissionDenied
return render(request,"messaging/revisions.html",{"message":msg,"revisions":msg.revisions.select_related("edited_by")})