first
This commit is contained in:
commit
fa9d154db9
260 files changed
+17643
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
from .models import Message,MessageMention,MessageRevision
|
||||
admin.site.register([Message,MessageMention,MessageRevision])
|
||||
@@ -0,0 +1,2 @@
|
||||
from django.apps import AppConfig
|
||||
class MessagingConfig(AppConfig): name="apps.messaging"
|
||||
@@ -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')],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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.
@@ -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
|
||||
@@ -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
|
||||
@@ -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")]
|
||||
|
||||
@@ -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")})
|
||||
Reference in new issue
Block a user