"""Staff conversation access; message grants never become ticket grants.""" import hashlib from django.core.exceptions import PermissionDenied from django.db.models import Q from django.http import JsonResponse from django.template.loader import render_to_string from .models import MessageShare, TicketAccessGrant from .policies import TicketPolicy from .sharing import staff_message_context def received_messages(user, ticket=None): grants = MessageShare.objects.filter(user=user, revoked_at__isnull=True, message__deleted_at__isnull=True) return grants.filter(message__ticket=ticket) if ticket is not None else grants def unread_shares(user): profile = getattr(user, "staff_profile", None) if not user.is_authenticated or not user.is_active or user.account_type != "STAFF" or not profile or not profile.is_active: return 0 return (received_messages(user).filter(first_viewed_at__isnull=True).count() + TicketAccessGrant.objects.filter(user=user, revoked_at__isnull=True, first_viewed_at__isnull=True).count()) def visible_messages(ticket, user): profile = getattr(user, "staff_profile", None) if not user.is_active or (profile is not None and not profile.is_active): raise PermissionDenied query = ticket.ticket_messages.all() if TicketPolicy.can_view(user, ticket): return query grants = received_messages(user, ticket) if not grants.exists(): raise PermissionDenied return query.filter(pk__in=grants.values("message_id")) def message_context(ticket, user, conversation): context = staff_message_context(ticket, user, conversation) grants = {g.message_id: g for g in received_messages(user, ticket).select_related("granted_by__staff_profile")} for item in conversation: item.received_share = grants.get(item.pk) context["limited_share"] = not TicketPolicy.can_view(user, ticket) return context def shared_ticket_rows(user, params): """All received shares use ordinary ticket metadata and filters.""" from .models import Ticket from .selectors import filter_staff_tickets, staff_ticket_rows full = staff_ticket_rows(user) query = Ticket.objects.select_related("customer__customer_profile", "category", "owner__staff_profile", "selected_expert__staff_profile", "owning_unit", "assigned_unit", "group_dispatch").filter( Q(pk__in=full.values("pk")) | Q(pk__in=received_messages(user).values("message__ticket_id"))) rows = list(filter_staff_tickets(query, user, params)[:100]) full_ids = set(full.filter(pk__in=[ticket.pk for ticket in rows]).values_list("pk", flat=True)) for ticket in rows: ticket.limited_share = ticket.pk not in full_ids return rows def limited_poll(request, ticket): """Version depends exclusively on authorized messages and their active grants.""" from .views import conversation_page query = visible_messages(ticket, request.user) grants = list(received_messages(request.user, ticket).values_list("pk", "note")) source = repr((list(query.order_by("pk").values_list("pk", "updated_at")), grants, ticket.status, ticket.priority, ticket.subject, ticket.province, ticket.city)) version = hashlib.sha256(source.encode()).hexdigest()[:20] latest = query.order_by("-created_at", "-pk").first() cursor = str(latest.public_id) if latest else "" data = {"changed": False, "version": version, "cursor": cursor} if request.GET.get("version") != version or request.GET.get("cursor", "") != cursor: conversation, has_older, older_cursor = conversation_page(query.select_related("author").prefetch_related("attachments")) context = {"ticket": ticket, "viewer_user": request.user, "conversation": conversation, "has_older": has_older, "older_cursor": older_cursor, **message_context(ticket, request.user, conversation)} data.update(changed=True, replace=True, staff_limited=True, count=query.count(), status=ticket.get_status_display(), html=render_to_string("tickets/_staff_older_messages.html", context, request=request)) response = JsonResponse(data) response["Cache-Control"] = "private, no-store" return response