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
+49
-23
@@ -14,6 +14,7 @@ from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.dateparse import parse_date
|
||||
from django.views.decorators.http import require_http_methods, require_POST
|
||||
from django.views.decorators.cache import never_cache
|
||||
from uuid import uuid4
|
||||
import hashlib
|
||||
import json
|
||||
@@ -30,11 +31,14 @@ from apps.core.iran_locations import location_data
|
||||
from apps.core.persian_text import normalize_persian_text
|
||||
from apps.integrations.voip import place_voip_call
|
||||
from apps.staff.models import OrganizationalUnit, StaffProfile
|
||||
from .forms import ContactGroupForm,GroupDispatchForm,ReplyForm,ShareForm,StaffContactForm,StaffCreateTicketForm,StaffReplyForm,TicketForm,TransferForm,UnitSMSTemplateForm
|
||||
from .models import GroupDispatch,Ticket,TicketCategory,TicketReadState,TicketTag
|
||||
from .forms import ContactGroupForm,GroupDispatchForm,ReplyForm,StaffContactForm,StaffCreateTicketForm,StaffReplyForm,TicketForm,TransferForm,UnitSMSTemplateForm
|
||||
from .models import GroupDispatch,MessageShare,Ticket,TicketCategory,TicketReadState,TicketTag
|
||||
from .forms import StaffMessageShareForm, StaffTicketShareForm
|
||||
from .sharing import mark_ticket_share_opened, require_share_authority, revoke_staff_share, share_staff_message, share_staff_ticket, sharing_context, staff_message_context
|
||||
from .policies import DispatchPolicy, TicketPolicy
|
||||
from .staff_conversation import limited_poll, message_context, shared_ticket_rows, visible_messages
|
||||
from .selectors import customer_tickets,dashboard_counts,filter_staff_tickets,staff_ticket_rows
|
||||
from .services import change_priority,change_status,claim_ticket,create_ticket,dispatch_group_tickets,refresh_group_dispatch_status,reply_ticket,resolve_group_dispatch_tickets,revoke_access,share_ticket,transfer_ticket
|
||||
from .services import change_priority,change_status,claim_ticket,create_ticket,dispatch_group_tickets,refresh_group_dispatch_status,reply_ticket,resolve_group_dispatch_tickets,transfer_ticket
|
||||
MESSAGE_PAGE_SIZE=50
|
||||
SMS_RETRYABLE_DELIVERY_STATUSES=(
|
||||
NotificationJob.DeliveryStatus.UNDELIVERED,
|
||||
@@ -145,7 +149,12 @@ def conversation_poll_response(request,ticket,actor,include_internal=False):
|
||||
else:
|
||||
conversation=list(messages_qs[:MESSAGE_PAGE_SIZE])
|
||||
if include_internal: mark_messages_read(conversation,actor)
|
||||
html=render_to_string("tickets/_messages.html",{"conversation":conversation,"ticket":ticket,"viewer_user":actor},request=request)
|
||||
context={"conversation":conversation,"ticket":ticket,"viewer_user":actor}
|
||||
template="tickets/_messages.html"
|
||||
if include_internal:
|
||||
context.update(message_context(ticket,actor,conversation))
|
||||
template="tickets/_staff_messages.html"
|
||||
html=render_to_string(template,context,request=request)
|
||||
response=JsonResponse({"changed":True,"version":version,"cursor":str(latest.public_id) if latest else "","replace":replace,"html":html,"count":qs.count(),"status":ticket.get_status_display()})
|
||||
response["Cache-Control"]="private, no-cache, no-store, must-revalidate"
|
||||
return response
|
||||
@@ -343,7 +352,7 @@ def customer_reply(request,public_id):
|
||||
@staff_required
|
||||
def staff_list(request):
|
||||
from apps.staff.models import OrganizationalUnit, StaffProfile
|
||||
qs=filter_staff_tickets(staff_ticket_rows(request.user),request.user,request.GET)
|
||||
qs=shared_ticket_rows(request.user,request.GET) if request.GET.get("tab") == "shared" else filter_staff_tickets(staff_ticket_rows(request.user),request.user,request.GET)
|
||||
tab=request.GET.get("tab","")
|
||||
tab_titles={"":"میز کار","new":"صف جدید","selected":"انتخابی من","mine":"تیکتهای من","shared":"اشتراکها","waiting_staff":"در انتظار اقدام من","waiting_customer":"در انتظار مشتری","resolved":"حلشدهها","closed":"بستهشدهها"}
|
||||
if tab not in tab_titles: tab=""
|
||||
@@ -352,7 +361,7 @@ def staff_list(request):
|
||||
selected_group_ids=[int(value) for value in request.GET.getlist("contact_group") if value.isdigit()]
|
||||
categories=TicketCategory.objects.filter(is_active=True,unit_id__in=unit_ids) if unit_ids else TicketCategory.objects.filter(is_active=True)
|
||||
experts=StaffProfile.objects.filter(is_active=True,user__unit_memberships__unit_id__in=unit_ids,user__unit_memberships__is_active=True).select_related("user").distinct() if unit_ids else StaffProfile.objects.filter(is_active=True).select_related("user")
|
||||
context={"tickets":qs[:100],"statuses":Ticket.Status.choices,"priorities":Ticket.Priority.choices,"delivery_types":Ticket.DeliveryType.choices,"stakeholder_types":CustomerProfile.StakeholderType.choices,"categories":categories,"experts":experts,"units":OrganizationalUnit.objects.filter(pk__in=unit_ids),"contact_groups":ContactGroup.objects.filter(unit_id__in=unit_ids,is_active=True),"selected_group_ids":selected_group_ids,"ticket_tags":TicketTag.objects.filter(is_active=True),"selected_tag_ids":selected_tag_ids,"counts":dashboard_counts(request.user),"active_tab":tab,"active_tab_title":tab_titles[tab]}
|
||||
context={"tickets":qs[:100],"statuses":Ticket.Status.choices,"priorities":Ticket.Priority.choices,"delivery_types":Ticket.DeliveryType.choices,"stakeholder_types":CustomerProfile.StakeholderType.choices,"categories":categories,"experts":experts,"units":OrganizationalUnit.objects.filter(pk__in=unit_ids),"contact_groups":ContactGroup.objects.filter(unit_id__in=unit_ids,is_active=True),"selected_group_ids":selected_group_ids,"ticket_tags":TicketTag.objects.filter(is_active=True),"selected_tag_ids":selected_tag_ids,"counts":dashboard_counts(request.user),"active_tab":tab,"active_tab_title":tab_titles[tab],"unread_message_shares":MessageShare.objects.filter(user=request.user,revoked_at__isnull=True,first_viewed_at__isnull=True,message__deleted_at__isnull=True).count()}
|
||||
return render(request,"tickets/_staff_ticket_results.html" if request.headers.get("HX-Request") else "tickets/staff_list.html",context)
|
||||
@staff_required
|
||||
def staff_create(request):
|
||||
@@ -433,15 +442,20 @@ def staff_customer_search(request):
|
||||
results.append({"id":str(customer.public_id),"mobile":customer.local_mobile,"name":str(profile) if profile else customer.local_mobile,"first_name":getattr(profile,"first_name","") or "","last_name":getattr(profile,"last_name","") or "","province":getattr(profile,"province","") or "","city":getattr(profile,"city","") or "","customer_type":profile.get_customer_type_display() if profile and profile.customer_type else "ثبت نشده","ticket_count":customer.ticket_count,"active_count":customer.active_count})
|
||||
return JsonResponse({"results":results})
|
||||
@staff_required
|
||||
@never_cache
|
||||
def staff_detail(request,public_id):
|
||||
ticket=get_object_or_404(Ticket.objects.select_related("customer__customer_profile","category","owner__staff_profile","selected_expert__staff_profile","group_dispatch__unit","group_dispatch__contact_group"),public_id=public_id)
|
||||
if not TicketPolicy.can_view(request.user,ticket): raise PermissionDenied
|
||||
conversation,has_older,older_cursor=conversation_page(ticket.ticket_messages.select_related("author").prefetch_related("attachments","read_receipts__user__staff_profile","read_receipts__user__customer_profile"))
|
||||
visible=visible_messages(ticket,request.user)
|
||||
conversation,has_older,older_cursor=conversation_page(visible.select_related("author").prefetch_related("attachments","read_receipts__user__staff_profile","read_receipts__user__customer_profile"))
|
||||
mark_messages_read(conversation,request.user)
|
||||
last=conversation[-1] if conversation else None; TicketReadState.objects.update_or_create(ticket=ticket,user=request.user,defaults={"last_read_at":timezone.now(),"last_read_message":last})
|
||||
mark_ticket_share_opened(ticket,request.user)
|
||||
message_sharing=message_context(ticket,request.user,conversation)
|
||||
if TicketPolicy.can_view(request.user,ticket):
|
||||
last=conversation[-1] if conversation else None
|
||||
TicketReadState.objects.update_or_create(ticket=ticket,user=request.user,defaults={"last_read_at":timezone.now(),"last_read_message":last})
|
||||
history=Ticket.objects.filter(customer=ticket.customer).exclude(pk=ticket.pk).select_related("owner__staff_profile","selected_expert__staff_profile").order_by("-last_activity_at")[:5]
|
||||
sms_jobs=list(ticket.notificationjob_set.filter(channel=NotificationJob.Channel.SMS).exclude(event_type="UNIT_SUPERVISOR_NEW_TICKET").order_by("-created_at"))
|
||||
announcement_jobs=list(AnnouncementJob.objects.filter(Q(message__ticket=ticket) | Q(ticket=ticket)).select_related("message").order_by("-created_at", "-pk"))
|
||||
announcement_jobs=list(AnnouncementJob.objects.filter(Q(message_id__in=visible.values("pk")) | Q(ticket=ticket)).select_related("message").order_by("-created_at", "-pk"))
|
||||
can_retry_sms=TicketPolicy.can_reply(request.user,ticket)
|
||||
can_request_announcement=request.user.account_type==User.AccountType.STAFF and can_retry_sms
|
||||
configuration=VoipConfiguration.load() if can_request_announcement else None
|
||||
@@ -452,7 +466,7 @@ def staff_detail(request,public_id):
|
||||
group_dispatch = ticket.group_dispatch
|
||||
can_resolve_dispatch = bool(group_dispatch and DispatchPolicy.can_view(request.user, group_dispatch) and (request.user.has_perm("tickets.change_ticket") or TicketPolicy.has_unit_permission(request.user, group_dispatch.unit, "tickets.change_ticket")))
|
||||
group_open_count = group_dispatch.tickets.exclude(status__in=(Ticket.Status.RESOLVED, Ticket.Status.CLOSED)).count() if group_dispatch else 0
|
||||
return render(request,"tickets/staff_detail.html",{"ticket":ticket,"conversation":conversation,"has_older":has_older,"older_cursor":older_cursor,"form":StaffReplyForm(),"transfer_form":TransferForm(),"share_form":ShareForm(),"can_claim":TicketPolicy.can_claim(request.user,ticket),"can_transfer":TicketPolicy.can_transfer(request.user,ticket),"can_share":TicketPolicy.can_share(request.user,ticket),"can_delete":TicketPolicy.can_delete(request.user,ticket),"can_resolve_dispatch":can_resolve_dispatch,"group_open_count":group_open_count,"statuses":Ticket.Status.choices,"priorities":Ticket.Priority.choices,"grants":ticket.access_grants.filter(revoked_at__isnull=True).select_related("user__staff_profile"),"customer_history":history,"sms_jobs":sms_jobs,"announcement_jobs":announcement_jobs,"can_request_announcement":can_request_announcement,"announcement_ready":announcement_ready,"viewer_user":request.user,"voip_call_methods":_voip_call_methods(request.user),"poll_version":conversation_version(ticket,latest),"poll_cursor":latest.public_id if latest else ""})
|
||||
return render(request,"tickets/staff_detail.html",{"ticket":ticket,"conversation":conversation,"has_older":has_older,"older_cursor":older_cursor,"form":StaffReplyForm(),"transfer_form":TransferForm(),"share_form":StaffTicketShareForm(actor=request.user),"can_claim":TicketPolicy.can_claim(request.user,ticket),"can_transfer":TicketPolicy.can_transfer(request.user,ticket),"can_share":TicketPolicy.can_share(request.user,ticket),"can_delete":TicketPolicy.can_delete(request.user,ticket),"can_resolve_dispatch":can_resolve_dispatch,"group_open_count":group_open_count,"statuses":Ticket.Status.choices,"priorities":Ticket.Priority.choices,"grants":ticket.access_grants.filter(revoked_at__isnull=True).select_related("user__staff_profile"),"customer_history":history,"sms_jobs":sms_jobs,"announcement_jobs":announcement_jobs,"can_request_announcement":can_request_announcement,"announcement_ready":announcement_ready,"viewer_user":request.user,"voip_call_methods":_voip_call_methods(request.user),"poll_version":conversation_version(ticket,latest),"poll_cursor":latest.public_id if latest else "",**message_sharing,**sharing_context(ticket,request.user)})
|
||||
|
||||
|
||||
@staff_required
|
||||
@@ -493,14 +507,14 @@ def staff_sms_retry(request,public_id,job_id):
|
||||
@staff_required
|
||||
def staff_messages(request,public_id):
|
||||
ticket=get_object_or_404(Ticket,public_id=public_id)
|
||||
if not TicketPolicy.can_view(request.user,ticket): raise PermissionDenied
|
||||
conversation,has_older,older_cursor=conversation_page(ticket.ticket_messages.select_related("author").prefetch_related("attachments","read_receipts__user__staff_profile","read_receipts__user__customer_profile"),request.GET.get("before"))
|
||||
conversation,has_older,older_cursor=conversation_page(visible_messages(ticket,request.user).select_related("author").prefetch_related("attachments","read_receipts__user__staff_profile","read_receipts__user__customer_profile"),request.GET.get("before"))
|
||||
mark_messages_read(conversation,request.user)
|
||||
return render(request,"tickets/_older_messages.html",{"ticket":ticket,"conversation":conversation,"has_older":has_older,"older_cursor":older_cursor,"older_route":"staff_tickets:messages","viewer_user":request.user})
|
||||
return render(request,"tickets/_staff_older_messages.html",{"ticket":ticket,"conversation":conversation,"has_older":has_older,"older_cursor":older_cursor,"viewer_user":request.user,**message_context(ticket,request.user,conversation)})
|
||||
@staff_required
|
||||
def staff_poll(request,public_id):
|
||||
ticket=get_object_or_404(Ticket,public_id=public_id)
|
||||
if not TicketPolicy.can_view(request.user,ticket): raise PermissionDenied
|
||||
visible_messages(ticket,request.user)
|
||||
if not TicketPolicy.can_view(request.user,ticket): return limited_poll(request,ticket)
|
||||
return conversation_poll_response(request,ticket,request.user,include_internal=True)
|
||||
@staff_required
|
||||
@require_POST
|
||||
@@ -532,19 +546,31 @@ def staff_transfer(request,public_id):
|
||||
return redirect("staff_tickets:detail",public_id=public_id)
|
||||
@staff_required
|
||||
@require_POST
|
||||
def staff_share(request,public_id):
|
||||
ticket=get_object_or_404(Ticket,public_id=public_id); form=ShareForm(request.POST)
|
||||
if form.is_valid():
|
||||
try: share_ticket(ticket,request.user,form.cleaned_data["expert"],form.cleaned_data["permission_level"]); messages.success(request,"دسترسی ایجاد شد.")
|
||||
except PermissionDenied: messages.error(request,"اجازه اشتراک این تیکت را ندارید.")
|
||||
def staff_share(request,public_id,message_id=None):
|
||||
from .sharing_views import submit_share_form
|
||||
ticket=get_object_or_404(Ticket,public_id=public_id)
|
||||
require_share_authority(request.user,ticket)
|
||||
if message_id is not None:
|
||||
selected_message=get_object_or_404(Message,public_id=message_id,ticket=ticket)
|
||||
form=StaffMessageShareForm(request.POST,actor=request.user)
|
||||
save=lambda: share_staff_message(selected_message,request.user,form.cleaned_data["expert"],form.cleaned_data["note"])
|
||||
success="فقط همین پیام و پیوستهای آن در صندوق اشتراکهای کارشناس قرار گرفت."
|
||||
else:
|
||||
form=StaffTicketShareForm(request.POST,actor=request.user)
|
||||
save=lambda: share_staff_ticket(ticket,request.user,form.cleaned_data["expert"],form.cleaned_data["permission_level"],form.cleaned_data["note"])
|
||||
success="اشتراک تیکت ثبت شد؛ وضعیت مشاهده در سوابق قابل پیگیری است."
|
||||
submit_share_form(request,form,save,success)
|
||||
return redirect("staff_tickets:detail",public_id=public_id)
|
||||
|
||||
@staff_required
|
||||
@require_POST
|
||||
def staff_revoke(request,public_id,user_id):
|
||||
ticket=get_object_or_404(Ticket,public_id=public_id); target=get_object_or_404(User,public_id=user_id)
|
||||
try: revoke_access(ticket,request.user,target); messages.success(request,"دسترسی کارشناس لغو شد.")
|
||||
except PermissionDenied: messages.error(request,"اجازه لغو این دسترسی را ندارید.")
|
||||
ticket=get_object_or_404(Ticket,public_id=public_id)
|
||||
require_share_authority(request.user,ticket)
|
||||
grant=ticket.access_grants.filter(user__public_id=user_id,revoked_at__isnull=True).first()
|
||||
if grant:
|
||||
revoke_staff_share(grant,request.user)
|
||||
messages.success(request,"دسترسی اشتراکی لغو شد؛ مجوزهای مستقل کارشناس تغییر نمیکنند.")
|
||||
return redirect("staff_tickets:detail",public_id=public_id)
|
||||
|
||||
@staff_required
|
||||
|
||||
Reference in new issue
Block a user