estelam ticket, varizha , others
This commit is contained in:
1 parent
35a1ec1794
commit
6b546aad76
71 files changed
+2865
-191
No files matched your search
+37
-33
@@ -38,7 +38,7 @@ from .sharing import mark_ticket_share_opened, require_share_authority, revoke_s
|
||||
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,transfer_ticket
|
||||
from .services import change_priority,change_status,claim_ticket,create_ticket,create_group_dispatch,refresh_group_dispatch_status,reply_ticket,resolve_group_dispatch_tickets,transfer_ticket
|
||||
MESSAGE_PAGE_SIZE=50
|
||||
SMS_RETRYABLE_DELIVERY_STATUSES=(
|
||||
NotificationJob.DeliveryStatus.UNDELIVERED,
|
||||
@@ -122,11 +122,14 @@ 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]
|
||||
from .dispatch_responses import response_control
|
||||
control = response_control(ticket.group_dispatch) if ticket.group_dispatch_id else None
|
||||
revision=hashlib.sha256(repr((edits, control.opened_at if control else None)).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]+':'+revision
|
||||
def conversation_poll_response(request,ticket,actor,include_internal=False):
|
||||
qs=ticket.ticket_messages.all()
|
||||
from .dispatch_responses import filter_response_messages
|
||||
qs=filter_response_messages(ticket.ticket_messages.all(), actor)
|
||||
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,include_internal=include_internal)
|
||||
@@ -458,7 +461,7 @@ def staff_detail(request,public_id):
|
||||
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_ticket_share_opened(ticket,request.user)
|
||||
message_sharing=message_context(ticket,request.user,conversation)
|
||||
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]
|
||||
history=Ticket.objects.filter(TicketPolicy.viewable_q(request.user), customer=ticket.customer).exclude(pk=ticket.pk).select_related("owner__staff_profile","selected_expert__staff_profile").order_by("-last_activity_at").distinct()[: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"))
|
||||
can_retry_sms=TicketPolicy.can_reply(request.user,ticket)
|
||||
@@ -469,6 +472,7 @@ def staff_detail(request,public_id):
|
||||
sms_job.sms_retryable=can_retry_sms and _sms_job_is_retryable(sms_job)
|
||||
latest=conversation[-1] if conversation else None
|
||||
group_dispatch = ticket.group_dispatch
|
||||
message_sharing["can_view_dispatch"] = bool(group_dispatch and DispatchPolicy.can_view(request.user, 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
|
||||
customer_notices = payment_selectors.notices_with_totals(
|
||||
@@ -613,10 +617,19 @@ def staff_delete(request, public_id):
|
||||
@staff_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def staff_dispatch_create(request):
|
||||
from .dispatch_presentation import dispatch_form_options
|
||||
|
||||
create_unit_ids = {
|
||||
unit.pk for unit in OrganizationalUnit.objects.filter(pk__in=TicketPolicy.unit_ids(request.user), is_active=True)
|
||||
if any(DispatchPolicy.can_create(request.user, unit, kind) for kind in GroupDispatch.OperationType.values)
|
||||
}
|
||||
base_dispatches = _visible_dispatches(request.user)
|
||||
if not create_unit_ids and (request.method == "POST" or not (DispatchPolicy.can_list(request.user) or base_dispatches.exists())):
|
||||
raise PermissionDenied
|
||||
form = GroupDispatchForm(request.POST or None, request.FILES or None, user=request.user)
|
||||
if request.method == "POST" and form.is_valid():
|
||||
try:
|
||||
dispatch, _ = dispatch_group_tickets(
|
||||
dispatch, _ = create_group_dispatch(
|
||||
user=request.user,
|
||||
unit=form.cleaned_data["unit"],
|
||||
contact_group=form.cleaned_data["contact_group"],
|
||||
@@ -624,6 +637,8 @@ def staff_dispatch_create(request):
|
||||
subject=form.cleaned_data["subject"],
|
||||
body=form.cleaned_data["body"],
|
||||
operation_type=form.cleaned_data["operation_type"],
|
||||
response_mode=form.cleaned_data["response_mode"],
|
||||
notify_opening_sms=form.cleaned_data["notify_opening_sms"],
|
||||
tags=form.cleaned_data["tags"],
|
||||
files=request.FILES.getlist("files"),
|
||||
)
|
||||
@@ -631,13 +646,6 @@ def staff_dispatch_create(request):
|
||||
return redirect("staff_tickets:dispatch_detail", public_id=dispatch.public_id)
|
||||
except (PermissionDenied, ValidationError) as exc:
|
||||
form.add_error(None, str(exc))
|
||||
create_unit_ids = {
|
||||
unit.pk for unit in OrganizationalUnit.objects.filter(pk__in=TicketPolicy.unit_ids(request.user), is_active=True)
|
||||
if any(DispatchPolicy.can_create(request.user, unit, kind) for kind in GroupDispatch.OperationType.values)
|
||||
}
|
||||
if not create_unit_ids:
|
||||
raise PermissionDenied
|
||||
base_dispatches = _visible_dispatches(request.user)
|
||||
metrics_row = base_dispatches.aggregate(total=Count("id"), recipients=Sum("recipient_count"))
|
||||
metrics = {
|
||||
"total": metrics_row["total"] or 0,
|
||||
@@ -672,19 +680,27 @@ def staff_dispatch_create(request):
|
||||
return render(request, "tickets/staff_dispatch_create.html", {
|
||||
"form": form, "dispatches": page.object_list, "page": page, "metrics": metrics,
|
||||
"units": units, "groups": groups, "dispatch_statuses": GroupDispatch.Status.choices,
|
||||
"dispatch_options": dispatch_form_options(request.user, units, groups),
|
||||
"can_create_dispatch": bool(create_unit_ids),
|
||||
"history_units": OrganizationalUnit.objects.filter(pk__in=base_dispatches.values("unit_id")),
|
||||
"history_groups": ContactGroup.objects.filter(pk__in=base_dispatches.values("contact_group_id")),
|
||||
"active_staff_section": "dispatches", "active_tab_title": "ارسال گروهی و ردیابی",
|
||||
})
|
||||
|
||||
|
||||
@staff_required
|
||||
def staff_dispatch_detail(request, public_id):
|
||||
from .dispatch_responses import response_control, can_open_responses, populate_report_responses
|
||||
dispatch = get_object_or_404(
|
||||
GroupDispatch.objects.select_related("unit", "contact_group", "created_by"),
|
||||
public_id=public_id,
|
||||
)
|
||||
if not DispatchPolicy.can_view(request.user, dispatch):
|
||||
raise PermissionDenied
|
||||
tickets = list(dispatch.tickets.select_related("customer__customer_profile").prefetch_related("magic_links", "ticket_messages", "notificationjob_set").order_by("customer_id"))
|
||||
control = response_control(dispatch)
|
||||
locked = bool(control and control.mode == "SEALED" and not control.opened_at)
|
||||
tickets = list(dispatch.tickets.select_related("customer__customer_profile").prefetch_related("magic_links", "notificationjob_set").order_by("customer_id"))
|
||||
populate_report_responses(dispatch, tickets, locked)
|
||||
delivery_failures = {
|
||||
NotificationJob.DeliveryStatus.UNDELIVERED,
|
||||
NotificationJob.DeliveryStatus.EXPIRED,
|
||||
@@ -696,7 +712,6 @@ def staff_dispatch_detail(request, public_id):
|
||||
for ticket in tickets:
|
||||
ticket.sms_job = next((job for job in ticket.notificationjob_set.all() if job.channel == NotificationJob.Channel.SMS and job.event_type == SMSTemplate.Event.STAFF_TICKET_CREATED), None)
|
||||
ticket.sms_retryable = bool(ticket.sms_job and (ticket.sms_job.status == NotificationJob.Status.FAILED or (ticket.sms_job.delivery_status in delivery_failures and not ticket.sms_job.delivery_next_check_at)))
|
||||
ticket.latest_customer_response = next((message for message in reversed(list(ticket.ticket_messages.all())) if message.author_id == ticket.customer_id), None)
|
||||
links = list(ticket.magic_links.all())
|
||||
ticket.magic_total_views = sum(link.view_count for link in links)
|
||||
ticket.magic_link_count = len(links)
|
||||
@@ -704,17 +719,17 @@ def staff_dispatch_detail(request, public_id):
|
||||
ticket.magic_last_used_at = max((link.used_at for link in links if link.used_at), default=None)
|
||||
job = ticket.sms_job
|
||||
if not job:
|
||||
ticket.sms_state_title, ticket.sms_state_hint, ticket.sms_state_tone = "در صف ثبت نشده", "برای این مخاطب job پیامک پیدا نشد.", "neutral"
|
||||
ticket.sms_state_title, ticket.sms_state_hint, ticket.sms_state_tone = "در صف ثبت نشده", "برای این مخاطب سابقه ارسال پیامک ثبت نشده است.", "neutral"
|
||||
elif job.status == NotificationJob.Status.FAILED:
|
||||
ticket.sms_state_title, ticket.sms_state_hint, ticket.sms_state_tone = "ارسال ناموفق", f"ارسال خودکار پس از {1 + settings.SMS_MAX_RETRIES} تلاش متوقف شده است.", "danger"
|
||||
ticket.sms_state_title, ticket.sms_state_hint, ticket.sms_state_tone = "ارسال ناموفق", "ارسال خودکار متوقف شده است؛ علت خطا و امکان تلاش مجدد را در ادامه بررسی کنید.", "danger"
|
||||
elif job.status == NotificationJob.Status.RETRYING:
|
||||
ticket.sms_state_title, ticket.sms_state_hint, ticket.sms_state_tone = "در انتظار تلاش مجدد", "worker پیامک را در موعد بعدی دوباره ارسال میکند.", "warning"
|
||||
ticket.sms_state_title, ticket.sms_state_hint, ticket.sms_state_tone = "در انتظار تلاش مجدد", "سامانه پیامک را در موعد بعدی دوباره ارسال میکند.", "warning"
|
||||
elif job.status in (NotificationJob.Status.PENDING, NotificationJob.Status.PROCESSING):
|
||||
ticket.sms_state_title, ticket.sms_state_hint, ticket.sms_state_tone = "در صف ارسال", "پیامک هنوز به درگاه تحویل نشده است.", "info"
|
||||
elif job.delivery_status == NotificationJob.DeliveryStatus.DELIVERED:
|
||||
ticket.sms_state_title, ticket.sms_state_hint, ticket.sms_state_tone = "تحویل به گوشی تأیید شد", "اپراتور دریافت پیامک توسط مخاطب را تأیید کرده است.", "success"
|
||||
elif job.delivery_status in (NotificationJob.DeliveryStatus.PENDING, NotificationJob.DeliveryStatus.UNKNOWN, NotificationJob.DeliveryStatus.UNDELIVERED, NotificationJob.DeliveryStatus.CHECK_FAILED) and job.delivery_next_check_at:
|
||||
ticket.sms_state_title, ticket.sms_state_hint, ticket.sms_state_tone = "در حال پیگیری تحویل", "نتیجه فعلی قطعی نیست و worker استعلام را ادامه میدهد.", "warning"
|
||||
ticket.sms_state_title, ticket.sms_state_hint, ticket.sms_state_tone = "در حال پیگیری تحویل", "نتیجه فعلی قطعی نیست؛ پیگیری خودکار تحویل ادامه دارد.", "warning"
|
||||
elif job.delivery_status == NotificationJob.DeliveryStatus.BLOCKED:
|
||||
ticket.sms_state_title, ticket.sms_state_hint, ticket.sms_state_tone = "دریافت پیامک مسدود است", "شماره مخاطب دریافت پیامک تبلیغاتی را بسته است.", "danger"
|
||||
elif job.delivery_status == NotificationJob.DeliveryStatus.EXPIRED:
|
||||
@@ -732,14 +747,15 @@ def staff_dispatch_detail(request, public_id):
|
||||
tracking = sms_jobs.filter(delivery_next_check_at__isnull=False).count()
|
||||
opened = TicketMagicLink.objects.filter(ticket__group_dispatch=dispatch, view_count__gt=0).values("ticket_id").distinct().count()
|
||||
total_views = TicketMagicLink.objects.filter(ticket__group_dispatch=dispatch).aggregate(total=Sum("view_count"))["total"] or 0
|
||||
replied = dispatch.tickets.filter(last_customer_message_at__isnull=False).count()
|
||||
replied = sum(bool(ticket.response_count) for ticket in tickets)
|
||||
refresh_group_dispatch_status(dispatch.pk); dispatch.refresh_from_db(fields=["status"])
|
||||
pending = sum(sms_counts.get(status, 0) for status in (NotificationJob.Status.PENDING, NotificationJob.Status.PROCESSING, NotificationJob.Status.RETRYING))
|
||||
delivery_percent = round((delivered / dispatch.recipient_count) * 100) if dispatch.recipient_count else 0
|
||||
metrics = {"sent": sent, "delivered": delivered, "failed": failed, "retrying": retrying, "tracking": tracking, "pending": pending, "opened": opened, "not_opened": max(dispatch.recipient_count-opened,0), "total_views": total_views, "replied": replied, "delivery_percent": delivery_percent}
|
||||
open_ticket_count = sum(ticket.status not in (Ticket.Status.RESOLVED, Ticket.Status.CLOSED) for ticket in tickets)
|
||||
can_resolve_dispatch = request.user.has_perm("tickets.change_ticket") or TicketPolicy.has_unit_permission(request.user, dispatch.unit, "tickets.change_ticket")
|
||||
return render(request, "tickets/staff_dispatch_detail.html", {"dispatch": dispatch, "tickets": tickets, "metrics": metrics, "open_ticket_count": open_ticket_count, "can_resolve_dispatch": can_resolve_dispatch, "sms_max_attempts": 1 + settings.SMS_MAX_RETRIES, "active_staff_section": "dispatches"})
|
||||
opening_jobs = NotificationJob.objects.filter(ticket__group_dispatch=dispatch, event_type="DISPATCH_RESPONSES_OPENED").values("status").annotate(count=Count("pk"))
|
||||
return render(request, "tickets/staff_dispatch_detail.html", {"dispatch": dispatch, "tickets": tickets, "metrics": metrics, "open_ticket_count": open_ticket_count, "can_resolve_dispatch": can_resolve_dispatch, "sms_max_attempts": 1 + settings.SMS_MAX_RETRIES, "active_staff_section": "dispatches", "response_control": control, "responses_locked": locked, "can_open_responses": can_open_responses(request.user, dispatch), "opening_jobs": list(opening_jobs)})
|
||||
|
||||
|
||||
@staff_required
|
||||
@@ -782,19 +798,7 @@ def _unit_ids_with_permission(user, permission):
|
||||
|
||||
|
||||
def _visible_dispatches(user):
|
||||
if user.is_superuser:
|
||||
return GroupDispatch.objects.all()
|
||||
visibility = Q(pk__in=[])
|
||||
memberships = list(user.unit_memberships.filter(is_active=True, unit__is_active=True).select_related("unit"))
|
||||
for operation_type in GroupDispatch.OperationType.values:
|
||||
if DispatchPolicy.can_view_all(user, operation_type):
|
||||
visibility |= Q(operation_type=operation_type)
|
||||
continue
|
||||
view_all_units = [item.unit_id for item in memberships if DispatchPolicy.can_view_all(user, operation_type, item.unit)]
|
||||
create_units = [item.unit_id for item in memberships if DispatchPolicy.can_create(user, item.unit, operation_type)]
|
||||
visibility |= Q(operation_type=operation_type, unit_id__in=view_all_units)
|
||||
visibility |= Q(operation_type=operation_type, unit_id__in=create_units, created_by=user)
|
||||
return GroupDispatch.objects.filter(visibility)
|
||||
return DispatchPolicy.visible_dispatches(user)
|
||||
|
||||
|
||||
CONTACT_AUDIT_FIELDS = {
|
||||
|
||||
Reference in new issue
Block a user