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, priority=-1, queue=-1, new_status=None, time_spent=None, due_date=None, new_checklists=None, message_id=None, customfields_form=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, ) 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 title and title != ticket.title: c = f.ticketchange_set.create( field=_("Title"), old_value=ticket.title, new_value=title, ) ticket.title = 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 # save custom fields and ticket changes if customfields_form and customfields_form.is_valid(): customfields_form.save(followup=f) 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