share ticket and ticket message
This commit is contained in:
1 parent
39e64854b8
commit
79fe1cddbb
35 files changed
+1742
-95
No files matched your search
@@ -0,0 +1,176 @@
|
||||
"""Staff sharing and audit services, isolated from customer workflows."""
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.staff.models import StaffProfile
|
||||
from .models import MessageShare, Ticket, TicketAccessGrant, TicketEvent
|
||||
from .policies import TicketPolicy
|
||||
|
||||
|
||||
def staff_name(user):
|
||||
profile = getattr(user, "staff_profile", None)
|
||||
return str(profile) if profile else user.username
|
||||
|
||||
|
||||
def share_recipients(actor):
|
||||
return StaffProfile.objects.filter(is_active=True, user__is_active=True, user__account_type="STAFF").exclude(user=actor).select_related("user", "primary_unit")
|
||||
|
||||
|
||||
def require_share_authority(actor, ticket):
|
||||
if actor.account_type != "STAFF" or not actor.is_active or not TicketPolicy.can_share(actor, ticket):
|
||||
raise PermissionDenied
|
||||
|
||||
|
||||
def validate_recipient(actor, target):
|
||||
if not share_recipients(actor).filter(user=target).exists():
|
||||
raise ValidationError("گیرنده باید یک کارشناس فعال دیگر باشد.")
|
||||
|
||||
|
||||
def log_share(grant, actor, event_type, action):
|
||||
is_message = isinstance(grant, MessageShare)
|
||||
ticket = grant.message.ticket if is_message else grant.ticket
|
||||
metadata = {"scope": "message" if is_message else "ticket", "grant_id": grant.pk,
|
||||
"user": grant.user_id, "recipient_name": staff_name(grant.user),
|
||||
"action": action, "note": grant.note}
|
||||
if is_message:
|
||||
metadata["message_public_id"] = str(grant.message.public_id)
|
||||
else:
|
||||
metadata["level"] = grant.permission_level
|
||||
return TicketEvent.objects.create(ticket=ticket, actor=actor, event_type=event_type, metadata=metadata)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def share_staff_ticket(ticket, actor, target, level, note=""):
|
||||
ticket = Ticket.objects.select_for_update().get(pk=ticket.pk)
|
||||
require_share_authority(actor, ticket)
|
||||
validate_recipient(actor, target)
|
||||
if level not in TicketAccessGrant.Level.values or len(note) > 500:
|
||||
raise ValidationError("سطح دسترسی یا توضیح اشتراک نامعتبر است.")
|
||||
grant = ticket.access_grants.filter(user=target, revoked_at__isnull=True).first()
|
||||
if grant and grant.permission_level == level and grant.note == note:
|
||||
return grant
|
||||
action = "updated" if grant else "created"
|
||||
if grant:
|
||||
grant.permission_level, grant.note = level, note
|
||||
grant.save(update_fields=["permission_level", "note"])
|
||||
else:
|
||||
grant = TicketAccessGrant.objects.create(ticket=ticket, user=target, granted_by=actor, permission_level=level, note=note)
|
||||
event = log_share(grant, actor, TicketEvent.Type.SHARED, action)
|
||||
# Preserve the existing staff ticket-sharing notification behavior.
|
||||
from apps.notifications.models import NotificationJob
|
||||
from apps.notifications.services import enqueue
|
||||
enqueue(NotificationJob.Channel.BALE, "SHARE", ticket, f"ticket:{ticket.pk}:share-event:{event.pk}", staff_user=target)
|
||||
return grant
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def share_staff_message(message, actor, target, note=""):
|
||||
ticket = Ticket.objects.select_for_update().get(pk=message.ticket_id)
|
||||
require_share_authority(actor, ticket)
|
||||
validate_recipient(actor, target)
|
||||
message.refresh_from_db()
|
||||
if message.deleted_at or len(note) > 500:
|
||||
raise ValidationError("پیام حذفشده قابل اشتراک نیست یا توضیح بیش از حد طولانی است.")
|
||||
grant = MessageShare.objects.filter(message=message, user=target, revoked_at__isnull=True).first()
|
||||
if grant and grant.note == note:
|
||||
return grant
|
||||
action = "updated" if grant else "created"
|
||||
if grant:
|
||||
grant.note = note
|
||||
grant.save(update_fields=["note"])
|
||||
else:
|
||||
grant = MessageShare.objects.create(message=message, user=target, granted_by=actor, note=note)
|
||||
log_share(grant, actor, TicketEvent.Type.MESSAGE_SHARED, action)
|
||||
return grant
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def revoke_staff_share(grant, actor):
|
||||
ticket_id = grant.message.ticket_id if isinstance(grant, MessageShare) else grant.ticket_id
|
||||
ticket = Ticket.objects.select_for_update().get(pk=ticket_id)
|
||||
require_share_authority(actor, ticket)
|
||||
grant.refresh_from_db()
|
||||
if grant.revoked_at:
|
||||
return False
|
||||
if isinstance(grant, TicketAccessGrant):
|
||||
from .services import revoke_access
|
||||
return bool(revoke_access(ticket, actor, grant.user))
|
||||
grant.revoked_at, grant.revoked_by = timezone.now(), actor
|
||||
grant.save(update_fields=["revoked_at", "revoked_by"])
|
||||
log_share(grant, actor, TicketEvent.Type.MESSAGE_SHARE_REVOKED, "revoked")
|
||||
return True
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def mark_share_opened(grant, actor):
|
||||
grant = type(grant).objects.select_for_update().get(pk=grant.pk)
|
||||
if grant.user_id != actor.pk or grant.revoked_at or (isinstance(grant, MessageShare) and grant.message.deleted_at):
|
||||
raise PermissionDenied
|
||||
now = timezone.now()
|
||||
if grant.first_viewed_at is None:
|
||||
grant.first_viewed_at = now
|
||||
event_type = TicketEvent.Type.MESSAGE_SHARE_OPENED if isinstance(grant, MessageShare) else TicketEvent.Type.SHARE_OPENED
|
||||
log_share(grant, actor, event_type, "opened")
|
||||
grant.last_viewed_at = now
|
||||
grant.view_count += 1
|
||||
grant.save(update_fields=["first_viewed_at", "last_viewed_at", "view_count"])
|
||||
return grant
|
||||
|
||||
|
||||
def mark_ticket_share_opened(ticket, actor):
|
||||
grant = ticket.access_grants.filter(user=actor, revoked_at__isnull=True).first()
|
||||
if grant:
|
||||
mark_share_opened(grant, actor)
|
||||
|
||||
|
||||
def staff_message_context(ticket, actor, conversation=()):
|
||||
can_share = TicketPolicy.can_share(actor, ticket)
|
||||
context = {"can_share_message": can_share, "share_experts": share_recipients(actor) if can_share else []}
|
||||
if not can_share:
|
||||
return context
|
||||
selected = {item.pk: item for item in conversation}
|
||||
for item in selected.values():
|
||||
item.staff_message_shares = []
|
||||
item.staff_share_events = []
|
||||
if not selected:
|
||||
return context
|
||||
grants = MessageShare.objects.filter(message__ticket=ticket, message_id__in=selected).select_related(
|
||||
"message", "user__staff_profile", "granted_by__staff_profile", "revoked_by__staff_profile")
|
||||
for grant in grants:
|
||||
selected[grant.message_id].staff_message_shares.append(grant)
|
||||
by_public_id = {str(item.public_id): item for item in selected.values() if item.staff_message_shares}
|
||||
if by_public_id:
|
||||
for event in sharing_events(ticket, message_ids=list(by_public_id)):
|
||||
by_public_id[event.metadata["message_public_id"]].staff_share_events.append(event)
|
||||
return context
|
||||
|
||||
|
||||
def sharing_context(ticket, actor):
|
||||
if not TicketPolicy.can_share(actor, ticket):
|
||||
return {}
|
||||
return {
|
||||
"grants": ticket.access_grants.select_related("user__staff_profile", "granted_by__staff_profile", "revoked_by__staff_profile").order_by("-created_at", "-pk"),
|
||||
"message_shares": MessageShare.objects.filter(message__ticket=ticket).select_related("message", "user__staff_profile", "granted_by__staff_profile", "revoked_by__staff_profile"),
|
||||
"sharing_events": sharing_events(ticket),
|
||||
}
|
||||
|
||||
|
||||
def sharing_events(ticket, message_ids=None):
|
||||
event_types = [TicketEvent.Type.SHARED, TicketEvent.Type.ACCESS_REVOKED, TicketEvent.Type.SHARE_OPENED,
|
||||
TicketEvent.Type.MESSAGE_SHARED, TicketEvent.Type.MESSAGE_SHARE_REVOKED,
|
||||
TicketEvent.Type.MESSAGE_SHARE_OPENED, TicketEvent.Type.TRANSFERRED]
|
||||
query = ticket.events.filter(event_type__in=event_types).select_related("actor__staff_profile").order_by("-created_at", "-pk")
|
||||
if message_ids is not None:
|
||||
query = query.filter(metadata__message_public_id__in=message_ids)
|
||||
else:
|
||||
query = query[:100]
|
||||
events = list(query)
|
||||
from apps.accounts.models import User
|
||||
user_ids = {e.metadata.get(key) for e in events for key in ("user", "from", "to") if e.metadata.get(key)}
|
||||
names = {u.pk: staff_name(u) for u in User.objects.filter(pk__in=user_ids).select_related("staff_profile")}
|
||||
for event in events:
|
||||
event.recipient_name = event.metadata.get("recipient_name") or names.get(event.metadata.get("user") or event.metadata.get("to"), "")
|
||||
event.previous_owner_name = names.get(event.metadata.get("from"), "")
|
||||
event.actor_name = staff_name(event.actor) if event.actor else "سامانه"
|
||||
return events
|
||||
Reference in new issue
Block a user