fix payement link and gallery image
This commit is contained in:
1 parent
de3135b588
commit
1cbbbbd0dc
24 files changed
+654
-116
No files matched your search
@@ -1,7 +1,7 @@
|
||||
from django.db.models import Case, F, IntegerField, Q, Value, When
|
||||
from django.db.models import Case, Exists, IntegerField, OuterRef, Q, Value, When
|
||||
from django.utils import timezone
|
||||
from django.utils.dateparse import parse_date
|
||||
from .models import Ticket
|
||||
from .models import Ticket, TicketReadState
|
||||
from .policies import TicketPolicy
|
||||
def customer_tickets(user): return Ticket.objects.filter(customer=user).select_related("category","owner__staff_profile","selected_expert__staff_profile")
|
||||
def staff_ticket_rows(user):
|
||||
@@ -56,7 +56,8 @@ def filter_staff_tickets(qs, user, params):
|
||||
if tab == "shared":
|
||||
qs=qs.filter(Q(access_grants__user=user,access_grants__revoked_at__isnull=True,access_grants__first_viewed_at__isnull=True) | Q(pk__in=received_messages(user).filter(first_viewed_at__isnull=True).values("message__ticket_id")))
|
||||
else:
|
||||
qs=qs.filter(Q(read_states__user=user,read_states__last_read_at__lt=F("last_activity_at"))|Q(read_states__isnull=True))
|
||||
read_by_user=TicketReadState.objects.filter(ticket_id=OuterRef('pk'),user=user,last_read_at__gte=OuterRef('last_activity_at'))
|
||||
qs=qs.alias(_read_by_current_user=Exists(read_by_user)).filter(_read_by_current_user=False)
|
||||
qs=qs.distinct()
|
||||
if tab in ("", "waiting_staff"):
|
||||
return qs.annotate(
|
||||
|
||||
@@ -3,6 +3,8 @@ import mimetypes
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.http import FileResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
@@ -11,10 +13,12 @@ from django.views.decorators.http import require_GET, require_POST
|
||||
|
||||
from apps.attachments.delivery import signed_media_redirect
|
||||
from apps.attachments.models import Attachment
|
||||
from .models import MessageShare, Ticket
|
||||
from .models import MessageShare, Ticket, TicketReadState
|
||||
from .policies import TicketPolicy
|
||||
from apps.messaging.read_receipts import message_read_ids
|
||||
from .staff_conversation import received_messages, unread_shares, visible_messages
|
||||
from .sharing import mark_share_opened, revoke_staff_share
|
||||
from .views import staff_required
|
||||
from .views import staff_required, mark_messages_read
|
||||
|
||||
|
||||
def received_message_share(request, share_id):
|
||||
@@ -66,18 +70,26 @@ def shared_message(request, share_id):
|
||||
@staff_required
|
||||
@require_POST
|
||||
@never_cache
|
||||
@transaction.atomic
|
||||
def read_shared_messages(request, public_id):
|
||||
ticket = get_object_or_404(Ticket, public_id=public_id)
|
||||
ticket = get_object_or_404(Ticket.objects.select_for_update(), public_id=public_id)
|
||||
allowed = visible_messages(ticket, request.user)
|
||||
ids = request.POST.getlist("message_ids")[:100]
|
||||
# Filter strings without parsing attacker-controlled UUIDs in the ORM.
|
||||
visible = {str(value) for value in allowed.values_list("public_id", flat=True)}
|
||||
accepted = set(ids) & visible
|
||||
try:
|
||||
ids = message_read_ids(request)
|
||||
except ValidationError:
|
||||
return JsonResponse({"ok": False, "error": "invalid_message_ids"}, status=400)
|
||||
acknowledged = list(allowed.filter(public_id__in=ids, deleted_at__isnull=True))
|
||||
mark_messages_read(acknowledged, request.user)
|
||||
accepted = [item.public_id for item in acknowledged]
|
||||
grants = received_messages(request.user, ticket).filter(message__public_id__in=accepted)
|
||||
for grant in grants:
|
||||
if grant.first_viewed_at is None:
|
||||
mark_share_opened(grant, request.user)
|
||||
return JsonResponse({"unread_shares": unread_shares(request.user)})
|
||||
latest = allowed.filter(deleted_at__isnull=True).order_by('-created_at', '-pk').first()
|
||||
if latest and latest.public_id in accepted and TicketPolicy.can_view(request.user, ticket):
|
||||
TicketReadState.objects.update_or_create(ticket=ticket, user=request.user,
|
||||
defaults={'last_read_at': timezone.now(), 'last_read_message': latest})
|
||||
return JsonResponse({"unread_shares": unread_shares(request.user), "read_ids": [str(value) for value in accepted]})
|
||||
|
||||
|
||||
@staff_required
|
||||
|
||||
+22
-21
@@ -8,7 +8,7 @@ from django.core.exceptions import PermissionDenied,ValidationError
|
||||
from django.http import FileResponse, HttpResponse, JsonResponse
|
||||
from django.template.loader import render_to_string
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, Prefetch, Q, Sum
|
||||
from django.db.models import Count, Max, Prefetch, Q, Sum
|
||||
from django.shortcuts import get_object_or_404,redirect,render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
@@ -32,7 +32,7 @@ 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,StaffContactForm,StaffCreateTicketForm,StaffReplyForm,TicketForm,TransferForm,UnitSMSTemplateForm
|
||||
from .models import GroupDispatch,MessageShare,Ticket,TicketCategory,TicketReadState,TicketTag
|
||||
from .models import GroupDispatch,MessageShare,Ticket,TicketCategory,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
|
||||
@@ -97,7 +97,7 @@ def conversation_page(queryset,before=None):
|
||||
return conversation,has_older,(conversation[0].public_id if conversation and has_older else None)
|
||||
def mark_messages_read(conversation,user):
|
||||
MessageReadReceipt.objects.bulk_create(
|
||||
[MessageReadReceipt(message=item,user=user) for item in conversation if item.author_id != user.id],
|
||||
[MessageReadReceipt(message=item,user=user) for item in conversation if item.author_id != user.id and not item.deleted_at],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
def customer_required(view):
|
||||
@@ -118,14 +118,18 @@ def customer_ticket_actor(request,ticket):
|
||||
return request.user,False
|
||||
link=get_ticket_magic_access(request,ticket)
|
||||
return (link.customer,True) if link else (None,False)
|
||||
def conversation_version(ticket,latest):
|
||||
def conversation_version(ticket,latest,include_internal=False):
|
||||
query=ticket.ticket_messages.all()
|
||||
if not include_internal: query=query.filter(is_internal=False)
|
||||
edits=query.aggregate(edited=Max('edited_at'),deleted=Max('deleted_at'))
|
||||
revision=hashlib.sha256(repr(edits).encode()).hexdigest()[:20]
|
||||
source=f"{ticket.updated_at.isoformat()}:{latest.public_id if latest else ''}:{latest.updated_at.isoformat() if latest else ''}"
|
||||
return hashlib.sha256(source.encode()).hexdigest()[:20]
|
||||
return hashlib.sha256(source.encode()).hexdigest()[:20]+':'+revision
|
||||
def conversation_poll_response(request,ticket,actor,include_internal=False):
|
||||
qs=ticket.ticket_messages.all()
|
||||
if not include_internal: qs=qs.filter(is_internal=False)
|
||||
latest=qs.order_by("-created_at","-id").only("id","public_id","updated_at").first()
|
||||
version=conversation_version(ticket,latest)
|
||||
version=conversation_version(ticket,latest,include_internal=include_internal)
|
||||
requested_cursor = request.GET.get("cursor", "")
|
||||
latest_cursor = str(latest.public_id) if latest else ""
|
||||
# A client can have the newest version token but still miss the DOM update
|
||||
@@ -136,7 +140,7 @@ def conversation_poll_response(request,ticket,actor,include_internal=False):
|
||||
response["Cache-Control"]="private, no-cache, no-store, must-revalidate"
|
||||
return response
|
||||
cursor=requested_cursor
|
||||
replace=not cursor or not latest or str(latest.public_id)==cursor
|
||||
replace=not cursor or not latest or str(latest.public_id)==cursor or request.GET.get('version','').partition(':')[2]!=version.partition(':')[2]
|
||||
messages_qs=qs.select_related("author").prefetch_related("attachments","read_receipts__user__staff_profile","read_receipts__user__customer_profile")
|
||||
if not replace:
|
||||
cursor_message=qs.filter(public_id=cursor).only("created_at","id").first()
|
||||
@@ -148,14 +152,14 @@ def conversation_poll_response(request,ticket,actor,include_internal=False):
|
||||
recent=list(messages_qs.order_by("-created_at","-id")[:MESSAGE_PAGE_SIZE]); conversation=list(reversed(recent))
|
||||
else:
|
||||
conversation=list(messages_qs[:MESSAGE_PAGE_SIZE])
|
||||
if include_internal: mark_messages_read(conversation,actor)
|
||||
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()})
|
||||
delivered = conversation[-1] if conversation else latest
|
||||
response=JsonResponse({"changed":True,"version":version,"cursor":str(delivered.public_id) if delivered 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
|
||||
@customer_required
|
||||
@@ -327,17 +331,19 @@ def customer_poll(request,public_id):
|
||||
if actor is None: return redirect_to_login(request.get_full_path(),reverse("accounts:login"))
|
||||
return conversation_poll_response(request,ticket,actor,include_internal=False)
|
||||
@require_POST
|
||||
@transaction.atomic
|
||||
def customer_read(request,public_id):
|
||||
ticket=get_object_or_404(Ticket,public_id=public_id)
|
||||
from apps.messaging.read_receipts import message_read_ids
|
||||
ticket=get_object_or_404(Ticket.objects.select_for_update(),public_id=public_id)
|
||||
actor,_=customer_ticket_actor(request,ticket)
|
||||
if actor is None: return JsonResponse({"ok":False},status=401)
|
||||
try: message_ids=json.loads(request.body or b"{}").get("message_ids",[])
|
||||
except (TypeError,ValueError): return JsonResponse({"ok":False,"error":"invalid_json"},status=400)
|
||||
messages_qs=ticket.ticket_messages.filter(public_id__in=message_ids,is_internal=False).exclude(author=actor)
|
||||
MessageReadReceipt.objects.bulk_create([MessageReadReceipt(message=item,user=actor) for item in messages_qs],ignore_conflicts=True)
|
||||
try: message_ids=message_read_ids(request)
|
||||
except ValidationError: return JsonResponse({"ok":False,"error":"invalid_message_ids"},status=400)
|
||||
acknowledged=list(ticket.ticket_messages.filter(public_id__in=message_ids,is_internal=False,deleted_at__isnull=True))
|
||||
mark_messages_read(acknowledged,actor)
|
||||
from apps.notifications.services import cancel_staff_reply_sms_if_read
|
||||
cancel_staff_reply_sms_if_read(ticket,actor)
|
||||
return JsonResponse({"ok":True,"read":messages_qs.count()})
|
||||
return JsonResponse({"ok":True,"read":sum(item.author_id != actor.pk for item in acknowledged),"read_ids":[str(item.public_id) for item in acknowledged]})
|
||||
@require_POST
|
||||
def customer_reply(request,public_id):
|
||||
ticket=get_object_or_404(Ticket,public_id=public_id); form=ReplyForm(request.POST,request.FILES)
|
||||
@@ -450,12 +456,8 @@ 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)
|
||||
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)
|
||||
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_id__in=visible.values("pk")) | Q(ticket=ticket)).select_related("message").order_by("-created_at", "-pk"))
|
||||
@@ -472,7 +474,7 @@ def staff_detail(request,public_id):
|
||||
customer_notices = payment_selectors.notices_with_totals(
|
||||
payment_policies.visible(PaymentNotice.objects.filter(customer=ticket.customer), request.user)
|
||||
).order_by('-created_at', '-pk')[:3]
|
||||
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,"customer_notices":customer_notices,"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)})
|
||||
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,"customer_notices":customer_notices,"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,include_internal=True),"poll_cursor":latest.public_id if latest else "",**message_sharing,**sharing_context(ticket,request.user)})
|
||||
|
||||
|
||||
@staff_required
|
||||
@@ -514,7 +516,6 @@ def staff_sms_retry(request,public_id,job_id):
|
||||
def staff_messages(request,public_id):
|
||||
ticket=get_object_or_404(Ticket,public_id=public_id)
|
||||
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/_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):
|
||||
|
||||
Reference in new issue
Block a user