mirror of
https://gitea.mueller.network/extern/django-helpdesk.git
synced 2025-08-02 11:49:37 +02:00
This is extremely useful when writing custom frontends on top of django helpdesk. This is similar to the custom_fields except it is normalized and attached to followups and not tickets. The fact that it is just JSON means that there is no need for updating the database with new CustomField entries making for more easily maintained custom frontends.
400 lines
12 KiB
Python
400 lines
12 KiB
Python
import typing
|
|
|
|
from django.core.exceptions import ValidationError
|
|
from django.contrib.auth import get_user_model
|
|
from django.utils import timezone
|
|
from django.utils.translation import gettext as _
|
|
|
|
from helpdesk.lib import safe_template_context
|
|
from helpdesk import settings as helpdesk_settings
|
|
from helpdesk.lib import process_attachments
|
|
from helpdesk.decorators import (
|
|
is_helpdesk_staff,
|
|
)
|
|
from helpdesk.models import (
|
|
FollowUp,
|
|
Ticket,
|
|
TicketCC,
|
|
)
|
|
from helpdesk.signals import update_ticket_done
|
|
|
|
User = get_user_model()
|
|
|
|
def add_staff_subscription(
|
|
user: User,
|
|
ticket: Ticket
|
|
) -> None:
|
|
"""Auto subscribe the staff member if that's what the settigs say and the
|
|
user is authenticated and a staff member"""
|
|
if helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE \
|
|
and user.is_authenticated \
|
|
and return_ticketccstring_and_show_subscribe(user, ticket)[1]:
|
|
subscribe_to_ticket_updates(ticket, user)
|
|
|
|
|
|
def return_ticketccstring_and_show_subscribe(user, ticket):
|
|
"""used in view_ticket() and followup_edit()"""
|
|
# create the ticketcc_string and check whether current user is already
|
|
# subscribed
|
|
username = user.get_username().upper()
|
|
try:
|
|
useremail = user.email.upper()
|
|
except AttributeError:
|
|
useremail = ""
|
|
strings_to_check = list()
|
|
strings_to_check.append(username)
|
|
strings_to_check.append(useremail)
|
|
|
|
ticketcc_string = ''
|
|
all_ticketcc = ticket.ticketcc_set.all()
|
|
counter_all_ticketcc = len(all_ticketcc) - 1
|
|
show_subscribe = True
|
|
for i, ticketcc in enumerate(all_ticketcc):
|
|
ticketcc_this_entry = str(ticketcc.display)
|
|
ticketcc_string += ticketcc_this_entry
|
|
if i < counter_all_ticketcc:
|
|
ticketcc_string += ', '
|
|
if strings_to_check.__contains__(ticketcc_this_entry.upper()):
|
|
show_subscribe = False
|
|
|
|
# check whether current user is a submitter or assigned to ticket
|
|
assignedto_username = str(ticket.assigned_to).upper()
|
|
strings_to_check = list()
|
|
if ticket.submitter_email is not None:
|
|
submitter_email = ticket.submitter_email.upper()
|
|
strings_to_check.append(submitter_email)
|
|
strings_to_check.append(assignedto_username)
|
|
if strings_to_check.__contains__(username) or strings_to_check.__contains__(useremail):
|
|
show_subscribe = False
|
|
|
|
return ticketcc_string, show_subscribe
|
|
|
|
|
|
def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, can_update=False):
|
|
|
|
if ticket is not None:
|
|
|
|
queryset = TicketCC.objects.filter(
|
|
ticket=ticket, user=user, email=email)
|
|
|
|
# Don't create duplicate entries for subscribers
|
|
if queryset.count() > 0:
|
|
return queryset.first()
|
|
|
|
if user is None and len(email) < 5:
|
|
raise ValidationError(
|
|
_('When you add somebody on Cc, you must provide either a User or a valid email. Email: %s' % email)
|
|
)
|
|
|
|
return ticket.ticketcc_set.create(
|
|
user=user,
|
|
email=email,
|
|
can_view=can_view,
|
|
can_update=can_update
|
|
)
|
|
|
|
|
|
def get_and_set_ticket_status(
|
|
new_status: int,
|
|
ticket: Ticket,
|
|
follow_up: FollowUp
|
|
) -> typing.Tuple[str, int]:
|
|
"""Performs comparision on previous status to new status,
|
|
updating the title as required.
|
|
|
|
Returns:
|
|
The old status as a display string, old status code string
|
|
"""
|
|
old_status_str = ticket.get_status_display()
|
|
old_status = ticket.status
|
|
if new_status != ticket.status:
|
|
ticket.status = new_status
|
|
ticket.save()
|
|
follow_up.new_status = new_status
|
|
if follow_up.title:
|
|
follow_up.title += ' and %s' % ticket.get_status_display()
|
|
else:
|
|
follow_up.title = '%s' % ticket.get_status_display()
|
|
|
|
if not follow_up.title:
|
|
if follow_up.comment:
|
|
follow_up.title = _('Comment')
|
|
else:
|
|
follow_up.title = _('Updated')
|
|
|
|
follow_up.save()
|
|
return old_status_str, old_status
|
|
|
|
|
|
def update_messages_sent_to_by_public_and_status(
|
|
public: bool,
|
|
ticket: Ticket,
|
|
follow_up: FollowUp,
|
|
context: str,
|
|
messages_sent_to: typing.Set[str],
|
|
files: typing.List[typing.Tuple[str, str]]
|
|
) -> Ticket:
|
|
"""Sets the status of the ticket"""
|
|
if public and (
|
|
follow_up.comment or (
|
|
follow_up.new_status in (
|
|
Ticket.RESOLVED_STATUS,
|
|
Ticket.CLOSED_STATUS
|
|
)
|
|
)
|
|
):
|
|
if follow_up.new_status == Ticket.RESOLVED_STATUS:
|
|
template = 'resolved_'
|
|
elif follow_up.new_status == Ticket.CLOSED_STATUS:
|
|
template = 'closed_'
|
|
else:
|
|
template = 'updated_'
|
|
|
|
roles = {
|
|
'submitter': (template + 'submitter', context),
|
|
'ticket_cc': (template + 'cc', context),
|
|
}
|
|
if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change:
|
|
roles['assigned_to'] = (template + 'cc', context)
|
|
messages_sent_to.update(
|
|
ticket.send(
|
|
roles,
|
|
dont_send_to=messages_sent_to,
|
|
fail_silently=True,
|
|
files=files
|
|
)
|
|
)
|
|
return ticket
|
|
|
|
|
|
def get_template_staff_and_template_cc(
|
|
reassigned, follow_up: FollowUp
|
|
) -> typing.Tuple[str, str]:
|
|
if reassigned:
|
|
template_staff = 'assigned_owner'
|
|
elif follow_up.new_status == Ticket.RESOLVED_STATUS:
|
|
template_staff = 'resolved_owner'
|
|
elif follow_up.new_status == Ticket.CLOSED_STATUS:
|
|
template_staff = 'closed_owner'
|
|
else:
|
|
template_staff = 'updated_owner'
|
|
if reassigned:
|
|
template_cc = 'assigned_cc'
|
|
elif follow_up.new_status == Ticket.RESOLVED_STATUS:
|
|
template_cc = 'resolved_cc'
|
|
elif follow_up.new_status == Ticket.CLOSED_STATUS:
|
|
template_cc = 'closed_cc'
|
|
else:
|
|
template_cc = 'updated_cc'
|
|
|
|
return template_staff, template_cc
|
|
|
|
|
|
def update_ticket(
|
|
user,
|
|
ticket,
|
|
title=None,
|
|
comment="",
|
|
files=None,
|
|
public=False,
|
|
owner=-1,
|
|
ticket_title=None,
|
|
priority=-1,
|
|
queue=-1,
|
|
new_status=None,
|
|
time_spent=None,
|
|
due_date=None,
|
|
new_checklists=None,
|
|
message_id=None,
|
|
extra_fields=None,
|
|
):
|
|
# We need to allow the 'ticket' and 'queue' contexts to be applied to the
|
|
# comment.
|
|
context = safe_template_context(ticket)
|
|
if title is None:
|
|
title = ticket.title
|
|
if priority == -1:
|
|
priority = ticket.priority
|
|
if queue == -1:
|
|
queue = ticket.queue.id
|
|
if new_status is None:
|
|
new_status = ticket.status
|
|
if new_checklists is None:
|
|
new_checklists = {}
|
|
|
|
from django.template import engines
|
|
template_func = engines['django'].from_string
|
|
# this prevents system from trying to render any template tags
|
|
# broken into two stages to prevent changes from first replace being themselves
|
|
# changed by the second replace due to conflicting syntax
|
|
comment = comment.replace(
|
|
'{%', 'X-HELPDESK-COMMENT-VERBATIM').replace('%}', 'X-HELPDESK-COMMENT-ENDVERBATIM')
|
|
comment = comment.replace(
|
|
'X-HELPDESK-COMMENT-VERBATIM', '{% verbatim %}{%'
|
|
).replace(
|
|
'X-HELPDESK-COMMENT-ENDVERBATIM', '%}{% endverbatim %}'
|
|
)
|
|
# render the neutralized template
|
|
comment = template_func(comment).render(context)
|
|
|
|
if owner == -1 and ticket.assigned_to:
|
|
owner = ticket.assigned_to.id
|
|
|
|
f = FollowUp(ticket=ticket, date=timezone.now(), comment=comment,
|
|
time_spent=time_spent, message_id=message_id, title=title,
|
|
extra_fields=extra_fields)
|
|
|
|
if is_helpdesk_staff(user):
|
|
f.user = user
|
|
|
|
f.public = public
|
|
|
|
reassigned = False
|
|
|
|
old_owner = ticket.assigned_to
|
|
if owner != -1:
|
|
if owner != 0 and ((ticket.assigned_to and owner != ticket.assigned_to.id) or not ticket.assigned_to):
|
|
new_user = User.objects.get(id=owner)
|
|
f.title = _('Assigned to %(username)s') % {
|
|
'username': new_user.get_username(),
|
|
}
|
|
ticket.assigned_to = new_user
|
|
reassigned = True
|
|
# user changed owner to 'unassign'
|
|
elif owner == 0 and ticket.assigned_to is not None:
|
|
f.title = _('Unassigned')
|
|
ticket.assigned_to = None
|
|
|
|
old_status_str, old_status = get_and_set_ticket_status(new_status, ticket, f)
|
|
|
|
files = process_attachments(f, files) if files else []
|
|
|
|
if ticket_title and ticket_title != ticket.title:
|
|
c = f.ticketchange_set.create(
|
|
field=_('Title'),
|
|
old_value=ticket.title,
|
|
new_value=ticket_title,
|
|
)
|
|
ticket.title = ticket_title
|
|
|
|
if new_status != old_status:
|
|
c = f.ticketchange_set.create(
|
|
field=_('Status'),
|
|
old_value=old_status_str,
|
|
new_value=ticket.get_status_display(),
|
|
)
|
|
|
|
if ticket.assigned_to != old_owner:
|
|
c = f.ticketchange_set.create(
|
|
field=_('Owner'),
|
|
old_value=old_owner,
|
|
new_value=ticket.assigned_to,
|
|
)
|
|
|
|
if priority != ticket.priority:
|
|
c = f.ticketchange_set.create(
|
|
field=_('Priority'),
|
|
old_value=ticket.priority,
|
|
new_value=priority,
|
|
)
|
|
ticket.priority = priority
|
|
|
|
if queue != ticket.queue.id:
|
|
c = f.ticketchange_set.create(
|
|
field=_('Queue'),
|
|
old_value=ticket.queue.id,
|
|
new_value=queue,
|
|
)
|
|
ticket.queue_id = queue
|
|
|
|
if due_date != ticket.due_date:
|
|
c = f.ticketchange_set.create(
|
|
field=_('Due on'),
|
|
old_value=ticket.due_date,
|
|
new_value=due_date,
|
|
)
|
|
ticket.due_date = due_date
|
|
|
|
for checklist in ticket.checklists.all():
|
|
if checklist.id not in new_checklists:
|
|
continue
|
|
new_completed_tasks = new_checklists[checklist.id]
|
|
for task in checklist.tasks.all():
|
|
changed = None
|
|
|
|
# Add completion if it was not done yet
|
|
if not task.completion_date and task.id in new_completed_tasks:
|
|
task.completion_date = timezone.now()
|
|
changed = 'completed'
|
|
# Remove it if it was done before
|
|
elif task.completion_date and task.id not in new_completed_tasks:
|
|
task.completion_date = None
|
|
changed = 'uncompleted'
|
|
|
|
# Save and add ticket change if task state has changed
|
|
if changed:
|
|
task.save(update_fields=['completion_date'])
|
|
f.ticketchange_set.create(
|
|
field=f'[{checklist.name}] {task.description}',
|
|
old_value=_('To do') if changed == 'completed' else _('Completed'),
|
|
new_value=_('Completed') if changed == 'completed' else _('To do'),
|
|
)
|
|
|
|
if new_status in (
|
|
Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS
|
|
) and (
|
|
new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None
|
|
):
|
|
ticket.resolution = comment
|
|
|
|
# ticket might have changed above, so we re-instantiate context with the
|
|
# (possibly) updated ticket.
|
|
context = safe_template_context(ticket)
|
|
context.update(
|
|
resolution=ticket.resolution,
|
|
comment=f.comment,
|
|
)
|
|
|
|
messages_sent_to = set()
|
|
try:
|
|
messages_sent_to.add(user.email)
|
|
except AttributeError:
|
|
pass
|
|
ticket = update_messages_sent_to_by_public_and_status(
|
|
public,
|
|
ticket,
|
|
f,
|
|
context,
|
|
messages_sent_to,
|
|
files
|
|
)
|
|
|
|
template_staff, template_cc = get_template_staff_and_template_cc(reassigned, f)
|
|
if ticket.assigned_to and (
|
|
ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change
|
|
or (reassigned and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign)
|
|
):
|
|
messages_sent_to.update(ticket.send(
|
|
{'assigned_to': (template_staff, context)},
|
|
dont_send_to=messages_sent_to,
|
|
fail_silently=True,
|
|
files=files,
|
|
))
|
|
|
|
messages_sent_to.update(ticket.send(
|
|
{'ticket_cc': (template_cc, context)},
|
|
dont_send_to=messages_sent_to,
|
|
fail_silently=True,
|
|
files=files,
|
|
))
|
|
ticket.save()
|
|
|
|
# emit signal with followup when the ticket update is done
|
|
# internally used for webhooks
|
|
update_ticket_done.send(sender="update_ticket", followup=f)
|
|
|
|
# auto subscribe user if enabled
|
|
add_staff_subscription(user, ticket)
|
|
return f
|
|
|