Merge pull request #895 from Benbb96/merging-tickets-feature

New feature:  Merging tickets
This commit is contained in:
Garret Wassermann 2020-11-01 13:54:03 -05:00 committed by GitHub
commit c9fa0c81c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 2119 additions and 1114 deletions

View File

@ -337,9 +337,15 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
if previous_followup is None and ticket_id is not None:
try:
ticket = Ticket.objects.get(id=ticket_id)
new = False
except Ticket.DoesNotExist:
ticket = None
else:
new = False
# Check if the ticket has been merged to another ticket
if ticket.merged_to:
logger.info("Ticket has been merged to %s" % ticket.merged_to.ticket)
# Use the ticket in which it was merged to for next operations
ticket = ticket.merged_to
# New issue, create a new <Ticket> instance
if ticket is None:

View File

@ -8,7 +8,7 @@ forms.py - Definitions of newforms-based forms for creating and maintaining
"""
import logging
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django import forms
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
@ -510,3 +510,22 @@ class TicketDependencyForm(forms.ModelForm):
class Meta:
model = TicketDependency
exclude = ('ticket',)
class MultipleTicketSelectForm(forms.Form):
tickets = forms.ModelMultipleChoiceField(
label=_('Tickets to merge'),
queryset=Ticket.objects.filter(merged_to=None),
widget=forms.SelectMultiple(attrs={'class': 'form-control'})
)
def clean_tickets(self):
tickets = self.cleaned_data.get('tickets')
if len(tickets) < 2:
raise ValidationError(_('Please choose at least 2 tickets.'))
if len(tickets) > 4:
raise ValidationError(_('Impossible to merge more than 4 tickets...'))
queues = tickets.order_by('queue').distinct().values_list('queue', flat=True)
if len(queues) != 1:
raise ValidationError(_('All selected tickets must share the same queue in order to be merged.'))
return tickets

View File

@ -26,7 +26,7 @@ def ticket_template_context(ticket):
for field in ('title', 'created', 'modified', 'submitter_email',
'status', 'get_status_display', 'on_hold', 'description',
'resolution', 'priority', 'get_priority_display',
'last_escalation', 'ticket', 'ticket_for_url',
'last_escalation', 'ticket', 'ticket_for_url', 'merged_to',
'get_status', 'ticket_url', 'staff_url', '_get_assigned_to'
):
attr = getattr(ticket, field, None)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.13 on 2020-10-27 17:14
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0032_kbitem_enabled'),
]
operations = [
migrations.AddField(
model_name='ticket',
name='merged_to',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='merged_tickets', to='helpdesk.Ticket', verbose_name='merged to'),
),
]

View File

@ -0,0 +1,57 @@
# Generated by Django 2.2.13 on 2020-10-29 22:34
from django.db import migrations
def forwards_func(apps, schema_editor):
EmailTemplate = apps.get_model("helpdesk", "EmailTemplate")
db_alias = schema_editor.connection.alias
EmailTemplate.objects.using(db_alias).create(
template_name='merged',
subject='(Merged)',
heading='Ticket merged',
plain_text="""Hello,
This is a courtesy e-mail to let you know that ticket {{ ticket.ticket }} ("{{ ticket.title }}") by {{ ticket.submitter_email }} has been merged to ticket {{ ticket.merged_to.ticket }}.
From now on, please answer on this ticket, or you can include the tag {{ ticket.merged_to.ticket }} in your e-mail subject.""",
html="""<p style="font-family: sans-serif; font-size: 1em;">Hello,</p>
<p style="font-family: sans-serif; font-size: 1em;">This is a courtesy e-mail to let you know that ticket <b>{{ ticket.ticket }}</b> (<em>{{ ticket.title }}</em>) by {{ ticket.submitter_email }} has been merged to ticket <a href="{{ ticket.merged_to.staff_url }}">{{ ticket.merged_to.ticket }}</a>.</p>
<p style="font-family: sans-serif; font-size: 1em;">From now on, please answer on this ticket, or you can include the tag <b>{{ ticket.merged_to.ticket }}</b> in your e-mail subject.</p>""",
locale='en'
)
EmailTemplate.objects.using(db_alias).create(
template_name='merged',
subject='(Fusionné)',
heading='Ticket Fusionné',
plain_text="""Bonjour,
Ce courriel indicatif permet de vous prévenir que le ticket {{ ticket.ticket }} ("{{ ticket.title }}") par {{ ticket.submitter_email }} a été fusionné au ticket {{ ticket.merged_to.ticket }}.
Veillez à répondre sur ce ticket dorénavant, ou bien inclure la balise {{ ticket.merged_to.ticket }} dans le sujet de votre réponse par mail.""",
html="""<p style="font-family: sans-serif; font-size: 1em;">Bonjour,</p>
<p style="font-family: sans-serif; font-size: 1em;">Ce courriel indicatif permet de vous prévenir que le ticket <b>{{ ticket.ticket }}</b> (<em>{{ ticket.title }}</em>) par {{ ticket.submitter_email }} a été fusionné au ticket <a href="{{ ticket.merged_to.staff_url }}">{{ ticket.merged_to.ticket }}</a>.</p>
<p style="font-family: sans-serif; font-size: 1em;">Veillez à répondre sur ce ticket dorénavant, ou bien inclure la balise <b>{{ ticket.merged_to.ticket }}</b> dans le sujet de votre réponse par mail.</p>""",
locale='fr'
)
def reverse_func(apps, schema_editor):
EmailTemplate = apps.get_model("helpdesk", "EmailTemplate")
db_alias = schema_editor.connection.alias
EmailTemplate.objects.using(db_alias).filter(template_name='merged').delete()
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0033_ticket_merged_to'),
]
operations = [
migrations.RunPython(forwards_func, reverse_func),
]

View File

@ -569,6 +569,15 @@ class Ticket(models.Model):
verbose_name=_('Knowledge base item the user was viewing when they created this ticket.'),
)
merged_to = models.ForeignKey(
'self',
verbose_name=_('merged to'),
related_name='merged_tickets',
on_delete=models.CASCADE,
null=True,
blank=True
)
@property
def time_spent(self):
"""Return back total time spent on the ticket. This is calculated value
@ -795,6 +804,46 @@ class Ticket(models.Model):
def get_resolution_markdown(self):
return get_markdown(self.resolution)
def add_email_to_ticketcc_if_not_in(self, email=None, user=None, ticketcc=None):
"""
Check that given email/user_email/ticketcc_email is not already present on the ticket
(submitter email, assigned to, or in ticket CCs) and add it to a new ticket CC,
or move the given one
:param str email:
:param User user:
:param TicketCC ticketcc:
:rtype: TicketCC|None
"""
if ticketcc:
email = ticketcc.display
elif user:
if user.email:
email = user.email
else:
# Ignore if user has no email address
return
elif not email:
raise ValueError('You must provide at least one parameter to get the email from')
# Prepare all emails already into the ticket
ticket_emails = [x.display for x in self.ticketcc_set.all()]
if self.submitter_email:
ticket_emails.append(self.submitter_email)
if self.assigned_to and self.assigned_to.email:
ticket_emails.append(self.assigned_to.email)
# Check that email is not already part of the ticket
if email not in ticket_emails:
if ticketcc:
ticketcc.ticket = self
ticketcc.save(update_fields=['ticket'])
elif user:
ticketcc = self.ticketcc_set.create(user=user)
else:
ticketcc = self.ticketcc_set.create(email=email)
return ticketcc
class FollowUpManager(models.Manager):

View File

@ -3,6 +3,7 @@ import mimetypes
import logging
from smtplib import SMTPException
from django.conf import settings
from django.utils.safestring import mark_safe
logger = logging.getLogger('helpdesk')
@ -72,7 +73,7 @@ def send_templated_mail(template_name,
footer_file = os.path.join('helpdesk', locale, 'email_text_footer.txt')
text_part = from_string(
"%s{%% include '%s' %%}" % (t.plain_text, footer_file)
"%s\n\n{%% include '%s' %%}" % (t.plain_text, footer_file)
).render(context)
email_html_base_file = os.path.join('helpdesk', locale, 'email_html_base.html')
@ -81,9 +82,9 @@ def send_templated_mail(template_name,
context['comment'] = mark_safe(context['comment'].replace('\r\n', '<br>'))
html_part = from_string(
"{%% extends '%s' %%}{%% block title %%}"
"%s"
"{%% endblock %%}{%% block content %%}%s{%% endblock %%}" %
"{%% extends '%s' %%}"
"{%% block title %%}%s{%% endblock %%}"
"{%% block content %%}%s{%% endblock %%}" %
(email_html_base_file, t.heading, t.html)
).render(context)

View File

@ -0,0 +1,13 @@
{% load helpdesk_util %}
<tr>
<td class="text-right">{{ display_attr }}</td>
{% for ticket in tickets %}
<td>
<label>
<input name="{{ attr }}" type="radio" value="{{ ticket.id }}"
class="flat"{% if ticket.values|get:attr|get:"checked" %} checked{% endif %}>
{{ ticket.values|get:attr|get:"value"|default:"Not defined"|linebreaksbr }}
</label>
</td>
{% endfor %}
</tr>

View File

@ -11,7 +11,7 @@
<thead class="thead-light">
<tr>
<th>{% trans "Ticket" %}</th>
<th>{% trans "Prority" %}</th>
<th>{% trans "Priority" %}</th>
<th>{% trans "Queue" %}</th>
<th>{% trans "Created" %}</th>
<th>{% trans "Actions" %}</th>
@ -51,7 +51,7 @@
<thead class="thead-light">
<tr>
<th>{% trans "Ticket" %}</th>
<th>{% trans "Prority" %}</th>
<th>{% trans "Priority" %}</th>
<th>{% trans "Queue" %}</th>
<th>{% trans "Created" %}</th>
<th>{% trans "Actions" %}</th>

View File

@ -21,196 +21,198 @@
{% endblock %}
{% block helpdesk_body %}
{% if helpdesk_settings.HELPDESK_TRANSLATE_TICKET_COMMENTS %}
{% comment %}
<div id='translate_dropdown'>{% trans "Translate ticket comments into" %} </div>
<div id='translate_block'>
{% endcomment %}
<div id="google_translate_element"></div>
<script src="//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"></script>
{% endif %}
{% if helpdesk_settings.HELPDESK_TRANSLATE_TICKET_COMMENTS %}
<div id="google_translate_element"></div>
<script src="//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"></script>
{% endif %}
{% include "helpdesk/ticket_desc_table.html" %}
{% include "helpdesk/ticket_desc_table.html" %}
{% if ticket.followup_set.all %}
{% load ticket_to_link %}
<div class="card mb-3">
<div class="card-header"><i class="fas fa-clock fa-fw fa-lg"></i>&nbsp;{% trans "Follow-Ups" %}</div>
<div class="card-body">
<div class="list-group">
{% for followup in ticket.followup_set.all %}
<div class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ followup.title }}</h5>
<small><i class="fas fa-clock"></i>&nbsp;<span class='byline text-info'>{% if followup.user %}by {{ followup.user }},{% endif %} <span title='{{ followup.date|date:"r" }}'>{{ followup.date|naturaltime }}</span>{% if followup.time_spent %}, <span>{% trans "time spent" %}: {{ followup.time_spent_formated }}</span>{% endif %} {% if not followup.public %} <span class='private'>({% trans "Private" %})</span>{% endif %}</span></small>
</div>
<p class="mb-1">
{% if followup.comment %}
<p>{{ followup.get_markdown|urlizetrunc:50|num_to_link|linebreaksbr }}</p>
{% endif %}
{% for change in followup.ticketchange_set.all %}
{% if forloop.first %}<div class='changes'><ul>{% endif %}
<li>{% blocktrans with change.field as field and change.old_value as old_value and change.new_value as new_value %}Changed {{ field }} from {{ old_value }} to {{ new_value }}.{% endblocktrans %}</li>
{% if forloop.last %}</ul></div>{% endif %}
{% endfor %}
{% for attachment in followup.followupattachment_set.all %}{% if forloop.first %}{% trans "Attachments" %}:<div class='attachments'><ul>{% endif %}
<li><a href='{{ attachment.file.url }}'>{{ attachment.filename }}</a> ({{ attachment.mime_type }}, {{ attachment.size|filesizeformat }})
{% if followup.user and request.user == followup.user %}
<a href='{% url 'helpdesk:attachment_del' ticket.id attachment.id %}'><button class="btn btn-danger btn-sm"><i class="fas fa-trash"></i></button></a>
{% endif %}
</li>
{% if forloop.last %}</ul></div>{% endif %}
{% endfor %}
</p>
<!--- ugly long test to suppress the following if it will be empty, to save vertical space -->
{% with possible=helpdesk_settings.HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP %}
{% if possible and followup.user and request.user == followup.user and not followup.ticketchange_set.all or possible and user.is_superuser and helpdesk_settings.HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP %}
<small>
{% if helpdesk_settings.HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP %}
{% if followup.user and request.user == followup.user and not followup.ticketchange_set.all %}
<a href="{% url 'helpdesk:followup_edit' ticket.id followup.id %}" class='followup-edit'><button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-edit"></i></button></a>
{% endif %}
{% endif %}
{% if user.is_superuser and helpdesk_settings.HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP %}
<a href="{% url 'helpdesk:followup_delete' ticket.id followup.id %}" class='followup-edit'><button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-trash"></i></button></a>
{% endif %}
</small>
{% endif %}{% endwith %}
</div>
<!-- /.list-group-item -->
{% endfor %}
</div>
<!-- /.list-group -->
{% if ticket.merged_to %}
<div class="card card-body bg-light">
<h3 class="text-center">
{% trans "This ticket has been merged into ticket" %}
<a href="{{ ticket.merged_to.get_absolute_url }}">{{ ticket.merged_to }}</a>
</h3>
</div>
<!-- /.card-body -->
</div>
<!-- /.card -->
{% endif %}
{% if helpdesk_settings.HELPDESK_TRANSLATE_TICKET_COMMENTS %}
</div>
{% endif %}
<div class="card mb-3">
<div class="card-header">{% trans "Respond to this ticket" %}</div>
<div class="card-body">
<form method='post' action='update/' enctype='multipart/form-data'>
<fieldset>
<dl>
{% if preset_replies %}
<dt><label for='id_preset'>{% trans "Use a Pre-set Reply" %}</label> <span class='form_optional'>{% trans "(Optional)" %}</span></dt>
<dd><select name='preset' id='id_preset'><option value=''>------</option>{% for preset in preset_replies %}<option value='{{ preset.id }}'>{{ preset.name }}</option>{% endfor %}</select></dd>
<dd class='form_help_text'>{% trans "Selecting a pre-set reply will over-write your comment below. You can then modify the pre-set reply to your liking before saving this update." %}</dd>
{% endif %}
<dt><label for='commentBox'>{% trans "Comment / Resolution" %}</label></dt>
<dd><textarea rows='8' cols='70' name='comment' id='commentBox'></textarea></dd>
<dd class='form_help_text'>{% trans "You can insert ticket and queue details in your message. For more information, see the <a href='../../help/context/'>context help page</a>." %}</dd>
<dt><label>{% trans "New Status" %}</label></dt>
{% if not ticket.can_be_resolved %}<dd>{% trans "This ticket cannot be resolved or closed until the tickets it depends on are resolved." %}</dd>{% endif %}
{% ifequal ticket.status 1 %}
<dd><div class="form-group">
<label for='st_open' class='active radio-inline'><input type='radio' name='new_status' value='1' id='st_open' checked='checked'>{% trans "Open" %} &raquo;</label>
<label for='st_resolved' class="radio-inline"><input type='radio' name='new_status' value='3' id='st_resolved'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Resolved" %} &raquo;</label>
<label for='st_closed' class="radio-inline"><input type='radio' name='new_status' value='4' id='st_closed'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Closed" %} &raquo;</label>
<label class="radio-inline" for='st_duplicate'><input type='radio' name='new_status' value='5' id='st_duplicate'>{% trans "Duplicate" %}</label>
</div></dd>
{% endifequal %}
{% ifequal ticket.status 2 %}
<dd><div class="form-group">
<label for='st_reopened' class='active radio-inline'><input type='radio' name='new_status' value='2' id='st_reopened' checked='checked'>{% trans "Reopened" %} &raquo;</label>
<label class="radio-inline" for='st_resolved'><input type='radio' name='new_status' value='3' id='st_resolved'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Resolved" %} &raquo;</label>
<label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Closed" %} &raquo;</label>
<label class="radio-inline" for='st_duplicate'><input type='radio' name='new_status' value='5' id='st_duplicate'>{% trans "Duplicate" %}</label>
</div></dd>
{% endifequal %}
{% ifequal ticket.status 3 %}
<dd><div class="form-group">
<label for='st_reopened' class="radio-inline"><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label>
<label for='st_resolved' class='active radio-inline'><input type='radio' name='new_status' value='3' id='st_resolved' checked='checked'>{% trans "Resolved" %} &raquo;</label>
<label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed'>{% trans "Closed" %}</label>
</div></dd>
{% endifequal %}
{% ifequal ticket.status 4 %}
<dd><div class="form-group"><label for='st_reopened' class="radio-inline"><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label>
<label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed' checked='checked'>{% trans "Closed" %}</label></div></dd>
{% endifequal %}
{% ifequal ticket.status 5 %}
<dd><div class="form-group">
<label class="radio-inline" for='st_reopened'><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label>
<label class="radio-inline" for='st_duplicate'><input type='radio' name='new_status' value='5' id='st_duplicate' checked='checked'>{% trans "Duplicate" %}</label>
</div></dd>
{% endifequal %}
{% if helpdesk_settings.HELPDESK_UPDATE_PUBLIC_DEFAULT %}
<input type='hidden' name='public' value='1'>
{% else %}
<dt>
<label for='id_public'>{% trans "Is this update public?" %}</label> <span class='form_optional'>{% trans "(Optional)" %}</span>
</dt>
<dd><input type='checkbox' name='public' value='1' checked='checked' />&nbsp; {% trans 'Yes, make this update public.' %}</dd>
<dd class='form_help_text'>{% trans "If this is public, the submitter will be e-mailed your comment or resolution." %}</dd>
{% endif %}
{% if user.is_staff %}
<dt>
<label for='id_time_spent'>{% trans "Time spent" %}</label> <span class='form_optional'>{% trans "(Optional)" %}</span>
</dt>
<dd><input name='time_spent' type="time" /></dd>
{% endif %}
</dl>
<p id='ShowFurtherOptPara'><button class="btn btn-warning btn-sm" id='ShowFurtherEditOptions'>{% trans "Change Further Details &raquo;" %}</button></p>
<div id='FurtherEditOptions' style='display: none;'>
<dl>
<dt><label for='id_title'>{% trans "Title" %}</label></dt>
<dd><input type='text' name='title' value='{{ ticket.title|escape }}' /></dd>
<dt><label for='id_owner'>{% trans "Owner" %}</label></dt>
<dd><select id='id_owner' name='owner'><option value='0'>{% trans "Unassign" %}</option>{% for u in active_users %}<option value='{{ u.id }}' {% ifequal u.id ticket.assigned_to.id %}selected{% endifequal %}>{{ u }}</option>{% endfor %}</select></dd>
<dt><label for='id_priority'>{% trans "Priority" %}</label></dt>
<dd><select id='id_priority' name='priority'>{% for p in priorities %}<option value='{{ p.0 }}'{% ifequal p.0 ticket.priority %} selected='selected'{% endifequal %}>{{ p.1 }}</option>{% endfor %}</select></dd>
<dt><label for='id_due_date'>{% trans "Due on" %}</label></dt>
<dd>{{ form.due_date }}</dd>
</dl>
</div>
<p id='ShowFileUploadPara'><button class="btn btn-warning btn-sm" id='ShowFileUpload'>{% trans "Attach File(s) &raquo;" %}</button></p>
<div id='FileUpload' style='display: none;'>
<dl>
<dt><label for='id_file'>{% trans "Attach a File" %}</label></dt>
<dd>
<div class="add_file_fields_wrap">
<button class="add_file_field_button btn btn-success btn-xs">{% trans "Add Another File" %}</button>
<div><label class='btn btn-primary btn-sm btn-file'>
Browse... <input type="file" name='attachment' id='file0' style='display: none;'/>
</label><span>&nbsp;</span><span id='selectedfilename0'>{% trans 'No files selected.' %}</span></div>
{% else %}
{% if ticket.followup_set.all %}
{% load ticket_to_link %}
<div class="card mb-3">
<div class="card-header"><i class="fas fa-clock fa-fw fa-lg"></i>&nbsp;{% trans "Follow-Ups" %}</div>
<div class="card-body">
<div class="list-group">
{% for followup in ticket.followup_set.all %}
<div class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ followup.title|num_to_link }}</h5>
<small><i class="fas fa-clock"></i>&nbsp;<span class='byline text-info'>{% if followup.user %}by {{ followup.user }},{% endif %} <span title='{{ followup.date|date:"r" }}'>{{ followup.date|naturaltime }}</span>{% if followup.time_spent %}, <span>{% trans "time spent" %}: {{ followup.time_spent_formated }}</span>{% endif %} {% if not followup.public %} <span class='private'>({% trans "Private" %})</span>{% endif %}</span></small>
</div>
<p class="mb-1">
{% if followup.comment %}
<p>{{ followup.get_markdown|urlizetrunc:50|num_to_link|linebreaksbr }}</p>
{% endif %}
{% for change in followup.ticketchange_set.all %}
{% if forloop.first %}<div class='changes'><ul>{% endif %}
<li>{% blocktrans with change.field as field and change.old_value as old_value and change.new_value as new_value %}Changed {{ field }} from {{ old_value }} to {{ new_value }}.{% endblocktrans %}</li>
{% if forloop.last %}</ul></div>{% endif %}
{% endfor %}
{% for attachment in followup.followupattachment_set.all %}{% if forloop.first %}{% trans "Attachments" %}:<div class='attachments'><ul>{% endif %}
<li><a href='{{ attachment.file.url }}'>{{ attachment.filename }}</a> ({{ attachment.mime_type }}, {{ attachment.size|filesizeformat }})
{% if followup.user and request.user == followup.user %}
<a href='{% url 'helpdesk:attachment_del' ticket.id attachment.id %}'><button class="btn btn-danger btn-sm"><i class="fas fa-trash"></i></button></a>
{% endif %}
</li>
{% if forloop.last %}</ul></div>{% endif %}
{% endfor %}
</p>
<!--- ugly long test to suppress the following if it will be empty, to save vertical space -->
{% with possible=helpdesk_settings.HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP %}
{% if possible and followup.user and request.user == followup.user and not followup.ticketchange_set.all or possible and user.is_superuser and helpdesk_settings.HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP %}
<small>
{% if helpdesk_settings.HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP %}
{% if followup.user and request.user == followup.user and not followup.ticketchange_set.all %}
<a href="{% url 'helpdesk:followup_edit' ticket.id followup.id %}" class='followup-edit'><button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-edit"></i></button></a>
{% endif %}
{% endif %}
{% if user.is_superuser and helpdesk_settings.HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP %}
<a href="{% url 'helpdesk:followup_delete' ticket.id followup.id %}" class='followup-edit'><button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-trash"></i></button></a>
{% endif %}
</small>
{% endif %}{% endwith %}
</div>
<!-- /.list-group-item -->
{% endfor %}
</div>
<!-- /.list-group -->
</div>
<!-- /.card-body -->
</div>
</dd>
</dl>
<!-- /.card -->
</div>
{% endif %}
</fieldset>
<div class="card mb-3">
<div class="card-header">{% trans "Respond to this ticket" %}</div>
<div class="card-body">
<button class="btn btn-primary float-right" type='submit'>{% trans "Update This Ticket" %}</button>
<form method='post' action='update/' enctype='multipart/form-data'>
{% csrf_token %}</form>
<fieldset>
<dl>
{% if preset_replies %}
<dt><label for='id_preset'>{% trans "Use a Pre-set Reply" %}</label> <span class='form_optional'>{% trans "(Optional)" %}</span></dt>
<dd><select name='preset' id='id_preset'><option value=''>------</option>{% for preset in preset_replies %}<option value='{{ preset.id }}'>{{ preset.name }}</option>{% endfor %}</select></dd>
<dd class='form_help_text'>{% trans "Selecting a pre-set reply will over-write your comment below. You can then modify the pre-set reply to your liking before saving this update." %}</dd>
{% endif %}
</div>
</div>
<dt><label for='commentBox'>{% trans "Comment / Resolution" %}</label></dt>
<dd><textarea rows='8' cols='70' name='comment' id='commentBox'></textarea></dd>
<dd class='form_help_text'>{% trans "You can insert ticket and queue details in your message. For more information, see the <a href='../../help/context/'>context help page</a>." %}</dd>
<dt><label>{% trans "New Status" %}</label></dt>
{% if not ticket.can_be_resolved %}<dd>{% trans "This ticket cannot be resolved or closed until the tickets it depends on are resolved." %}</dd>{% endif %}
{% ifequal ticket.status 1 %}
<dd><div class="form-group">
<label for='st_open' class='active radio-inline'><input type='radio' name='new_status' value='1' id='st_open' checked='checked'>{% trans "Open" %} &raquo;</label>
<label for='st_resolved' class="radio-inline"><input type='radio' name='new_status' value='3' id='st_resolved'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Resolved" %} &raquo;</label>
<label for='st_closed' class="radio-inline"><input type='radio' name='new_status' value='4' id='st_closed'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Closed" %} &raquo;</label>
<label class="radio-inline" for='st_duplicate'><input type='radio' name='new_status' value='5' id='st_duplicate'>{% trans "Duplicate" %}</label>
</div></dd>
{% endifequal %}
{% ifequal ticket.status 2 %}
<dd><div class="form-group">
<label for='st_reopened' class='active radio-inline'><input type='radio' name='new_status' value='2' id='st_reopened' checked='checked'>{% trans "Reopened" %} &raquo;</label>
<label class="radio-inline" for='st_resolved'><input type='radio' name='new_status' value='3' id='st_resolved'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Resolved" %} &raquo;</label>
<label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Closed" %} &raquo;</label>
<label class="radio-inline" for='st_duplicate'><input type='radio' name='new_status' value='5' id='st_duplicate'>{% trans "Duplicate" %}</label>
</div></dd>
{% endifequal %}
{% ifequal ticket.status 3 %}
<dd><div class="form-group">
<label for='st_reopened' class="radio-inline"><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label>
<label for='st_resolved' class='active radio-inline'><input type='radio' name='new_status' value='3' id='st_resolved' checked='checked'>{% trans "Resolved" %} &raquo;</label>
<label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed'>{% trans "Closed" %}</label>
</div></dd>
{% endifequal %}
{% ifequal ticket.status 4 %}
<dd><div class="form-group"><label for='st_reopened' class="radio-inline"><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label>
<label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed' checked='checked'>{% trans "Closed" %}</label></div></dd>
{% endifequal %}
{% ifequal ticket.status 5 %}
<dd><div class="form-group">
<label class="radio-inline" for='st_reopened'><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label>
<label class="radio-inline" for='st_duplicate'><input type='radio' name='new_status' value='5' id='st_duplicate' checked='checked'>{% trans "Duplicate" %}</label>
</div></dd>
{% endifequal %}
{% if helpdesk_settings.HELPDESK_UPDATE_PUBLIC_DEFAULT %}
<input type='hidden' name='public' value='1'>
{% else %}
<dt>
<label for='id_public'>{% trans "Is this update public?" %}</label> <span class='form_optional'>{% trans "(Optional)" %}</span>
</dt>
<dd><input type='checkbox' name='public' value='1' checked='checked' />&nbsp; {% trans 'Yes, make this update public.' %}</dd>
<dd class='form_help_text'>{% trans "If this is public, the submitter will be e-mailed your comment or resolution." %}</dd>
{% endif %}
{% if user.is_staff %}
<dt>
<label for='id_time_spent'>{% trans "Time spent" %}</label> <span class='form_optional'>{% trans "(Optional)" %}</span>
</dt>
<dd><input name='time_spent' type="time" /></dd>
{% endif %}
</dl>
<p id='ShowFurtherOptPara'><button class="btn btn-warning btn-sm" id='ShowFurtherEditOptions'>{% trans "Change Further Details &raquo;" %}</button></p>
<div id='FurtherEditOptions' style='display: none;'>
<dl>
<dt><label for='id_title'>{% trans "Title" %}</label></dt>
<dd><input type='text' name='title' value='{{ ticket.title|escape }}' /></dd>
<dt><label for='id_owner'>{% trans "Owner" %}</label></dt>
<dd><select id='id_owner' name='owner'><option value='0'>{% trans "Unassign" %}</option>{% for u in active_users %}<option value='{{ u.id }}' {% ifequal u.id ticket.assigned_to.id %}selected{% endifequal %}>{{ u }}</option>{% endfor %}</select></dd>
<dt><label for='id_priority'>{% trans "Priority" %}</label></dt>
<dd><select id='id_priority' name='priority'>{% for p in priorities %}<option value='{{ p.0 }}'{% ifequal p.0 ticket.priority %} selected='selected'{% endifequal %}>{{ p.1 }}</option>{% endfor %}</select></dd>
<dt><label for='id_due_date'>{% trans "Due on" %}</label></dt>
<dd>{{ form.due_date }}</dd>
</dl>
</div>
<p id='ShowFileUploadPara'><button class="btn btn-warning btn-sm" id='ShowFileUpload'>{% trans "Attach File(s) &raquo;" %}</button></p>
<div id='FileUpload' style='display: none;'>
<dl>
<dt><label for='id_file'>{% trans "Attach a File" %}</label></dt>
<dd>
<div class="add_file_fields_wrap">
<button class="add_file_field_button btn btn-success btn-xs">{% trans "Add Another File" %}</button>
<div><label class='btn btn-primary btn-sm btn-file'>
Browse... <input type="file" name='attachment' id='file0' style='display: none;'/>
</label><span>&nbsp;</span><span id='selectedfilename0'>{% trans 'No files selected.' %}</span></div>
</div>
</dd>
</dl>
</div>
</fieldset>
<button class="btn btn-primary float-right" type='submit'>{% trans "Update This Ticket" %}</button>
{% csrf_token %}</form>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -21,7 +21,7 @@
{% for customfield in ticket.ticketcustomfieldvalue_set.all %}
<tr>
<th class="table-secondary">{{ customfield.field.label }}</th>
<td>{% ifequal customfield.field.data_type "url" %}<a href='{{ customfield.value }}'>{{ customfield.value }}</a>{% else %}{{ customfield.value }}{% endifequal %}</td>
<td>{% ifequal customfield.field.data_type "url" %}<a href='{{ customfield.value }}'>{{ customfield.value }}</a>{% else %}{{ customfield.value|default:"" }}{% endifequal %}</td>
</tr>{% endfor %}
<tr>
<th class="table-active">{% trans "Due Date" %}</th>

View File

@ -59,7 +59,7 @@
<tr>
<th>&nbsp;</th>
<th>{% trans "Ticket" %}</th>
<th>{% trans "Prority" %}</th>
<th>{% trans "Priority" %}</th>
<th>{% trans "Queue" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Created" %}</th>
@ -88,6 +88,7 @@
<select name='action' id='id_mass_action'>
<option value='take'>{% trans "Take (Assign to me)" %}</option>
<option value='delete'>{% trans "Delete" %}</option>
<option value='merge'>{% trans "Merge" %}</option>
<optgroup label='{% trans "Close" %}'>
<option value='close'>{% trans "Close (Don't Send E-Mail)" %}</option>
<option value='close_public'>{% trans "Close (Send E-Mail)" %}</option>

View File

@ -0,0 +1,105 @@
{% extends "helpdesk/base.html" %}
{% load i18n helpdesk_util %}
{% block helpdesk_title %}{% trans "Merge Tickets" %}{% endblock %}
{% block h1_title %}{% endblock %}
{% block helpdesk_head %}
{{ ticket_select_form.media.css }}
{% endblock %}
{% block helpdesk_breadcrumb %}
<li class="breadcrumb-item">
<a href="{% url 'helpdesk:list' %}">{% trans "Tickets" %}</a>
</li>
<li class="breadcrumb-item active">{% trans "Merge Tickets" %}</li>
{% endblock %}
{% block helpdesk_body %}
<div class="row">
<div class="col-sm-6 col-xs-12">
<div class="card">
<div class="card-header">
<h3>{% trans "Merge Tickets" %}</h3>
</div>
<div class="card-body">
<form method="get">
<div class="form-group">
{{ ticket_select_form.tickets.label_tag }}
{{ ticket_select_form.tickets }}
{{ ticket_select_form.tickets.errors }}
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary">{% trans "OK" %}</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-sm-6 col-xs-12">
<div class="card card-body bg-light">
<p>
{% blocktrans %}
Choose the ticket which will be conserved and then, for each information, you can decide to use
a data from another ticket to merge. I you don't select a data on a row, <b>the information
from the main ticket will stay unchanged</b>.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
The <b>follow-ups</b> and <b>attachments</b> from the merged tickets will be moved to
the main ticket.<br>
<b>Involved users</b> (the ticket submitter and emails in CC) will also be added in the
main ticket CC list.<br>
However, ticket dependencies from the merged ticket won't be applied to the main ticket.
{% endblocktrans %}
</p>
</div>
</div>
</div>
{% if tickets %}
<hr>
<form method="post">
{% csrf_token %}
<div class="table-responsive">
<table class="table table-striped table-hover table-bordered">
<thead class="thead-dark">
<tr>
<th class="text-center">{% trans "Fields" %}</th>
{% for ticket in tickets %}
<th class="text-center">
<label>
<input name="chosen_ticket" type="radio" value="{{ ticket.id }}"{% if forloop.first %} checked{% endif %}>
{{ ticket }}
<a href="{{ ticket.get_absolute_url }}" target="_blank" class="btn btn-outline-primary btn-sm" role="button">
{% trans "Open ticket" %}
<i class="fa fa-external-link-alt"></i>
</a>
</label>
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for attr, display_attr in ticket_attributes %}
{% include 'helpdesk/include/ticket_merge_row.html' %}
{% endfor %}
{% for custom_field in custom_fields %}
{% include 'helpdesk/include/ticket_merge_row.html' with display_attr=custom_field.label attr=custom_field.name %}
{% endfor %}
</tbody>
</table>
</div>
<div class="text-center">
<button type="submit" class="btn btn-lg btn-primary">{% trans "Validate" %}</button>
</div>
</form>
{% endif %}
{% endblock %}
{% block helpdesk_js %}
{{ ticket_select_form.media.js }}
{% endblock %}

View File

@ -0,0 +1,9 @@
from django import template
register = template.Library()
@register.filter
def get(value, arg, default=None):
""" Call the dictionary get function """
return value.get(arg, default)

View File

@ -4,6 +4,8 @@ from django.core import mail
from django.urls import reverse
from django.test import TestCase
from django.test.client import Client
from django.utils import timezone
from helpdesk.models import CustomField, Queue, Ticket
from helpdesk import settings as helpdesk_settings
@ -206,3 +208,100 @@ class TicketActionsTestCase(TestCase):
self.assertEqual(response.status_code, 200)
# TODO this needs to be checked further
def test_merge_tickets(self):
self.loginUser()
# Create two tickets
ticket_1 = Ticket.objects.create(
queue=self.queue_public,
title='Ticket 1',
description='Description from ticket 1',
submitter_email='user1@mail.com',
status=Ticket.RESOLVED_STATUS,
resolution='Awesome resolution for ticket 1'
)
ticket_1_follow_up = ticket_1.followup_set.create(title='Ticket 1 creation')
ticket_1_cc = ticket_1.ticketcc_set.create(user=self.user)
ticket_1_created = ticket_1.created
due_date = timezone.now()
ticket_2 = Ticket.objects.create(
queue=self.queue_public,
title='Ticket 2',
description='Description from ticket 2',
submitter_email='user2@mail.com',
due_date=due_date,
assigned_to=self.user
)
ticket_2_follow_up = ticket_1.followup_set.create(title='Ticket 2 creation')
ticket_2_cc = ticket_2.ticketcc_set.create(email='random@mail.com')
# Create custom fields and set values for tickets
custom_field_1 = CustomField.objects.create(
name='test',
label='Test',
data_type='varchar',
)
ticket_1_field_1 = 'This is for the test field'
ticket_1.ticketcustomfieldvalue_set.create(field=custom_field_1, value=ticket_1_field_1)
ticket_2_field_1 = 'Another test text'
ticket_2.ticketcustomfieldvalue_set.create(field=custom_field_1, value=ticket_2_field_1)
custom_field_2 = CustomField.objects.create(
name='number',
label='Number',
data_type='integer',
)
ticket_2_field_2 = '444'
ticket_2.ticketcustomfieldvalue_set.create(field=custom_field_2, value=ticket_2_field_2)
# Check that it correctly redirects to the intermediate page
response = self.client.post(
reverse('helpdesk:mass_update'),
data={
'ticket_id': [str(ticket_1.id), str(ticket_2.id)],
'action': 'merge'
},
follow=True
)
redirect_url = '%s?tickets=%s&tickets=%s' % (reverse('helpdesk:merge_tickets'), ticket_1.id, ticket_2.id)
self.assertRedirects(response, redirect_url)
self.assertContains(response, ticket_1.description)
self.assertContains(response, ticket_1.resolution)
self.assertContains(response, ticket_1.submitter_email)
self.assertContains(response, ticket_1_field_1)
self.assertContains(response, ticket_2.description)
self.assertContains(response, ticket_2.submitter_email)
self.assertContains(response, ticket_2_field_1)
self.assertContains(response, ticket_2_field_2)
# Check that the merge is correctly done
response = self.client.post(
redirect_url,
data={
'chosen_ticket': str(ticket_1.id),
'due_date': str(ticket_2.id),
'status': str(ticket_1.id),
'submitter_email': str(ticket_2.id),
'description': str(ticket_2.id),
'assigned_to': str(ticket_2.id),
custom_field_1.name: str(ticket_1.id),
custom_field_2.name: str(ticket_2.id),
},
follow=True
)
self.assertRedirects(response, ticket_1.get_absolute_url())
ticket_2.refresh_from_db()
self.assertEqual(ticket_2.merged_to, ticket_1)
self.assertEqual(ticket_2.followup_set.count(), 0)
self.assertEqual(ticket_2.ticketcc_set.count(), 0)
ticket_1.refresh_from_db()
self.assertEqual(ticket_1.created, ticket_1_created)
self.assertEqual(ticket_1.due_date, due_date)
self.assertEqual(ticket_1.status, Ticket.RESOLVED_STATUS)
self.assertEqual(ticket_1.submitter_email, ticket_2.submitter_email)
self.assertEqual(ticket_1.description, ticket_2.description)
self.assertEqual(ticket_1.assigned_to, ticket_2.assigned_to)
self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(field=custom_field_1).value, ticket_1_field_1)
self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(field=custom_field_2).value, ticket_2_field_2)
self.assertEqual(list(ticket_1.followup_set.all()), [ticket_1_follow_up, ticket_2_follow_up])
self.assertEqual(list(ticket_1.ticketcc_set.all()), [ticket_1_cc, ticket_2_cc])

View File

@ -1,10 +1,14 @@
# -*- coding: utf-8 -*-
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.test import TestCase
from helpdesk.models import Ticket, Queue
from django.test.utils import override_settings
User = get_user_model()
@override_settings(
HELPDESK_VIEW_A_TICKET_PUBLIC=True
)
@ -50,3 +54,42 @@ class TestTicketLookupPublicEnabled(TestCase):
# confirm that we can still get to a url which was emailed earlier
response = self.client.get(url, params)
self.assertNotContains(response, "Invalid ticket ID")
def test_add_email_to_ticketcc_if_not_in(self):
staff_email = 'staff@mail.com'
staff_user = User.objects.create(username='staff', email=staff_email, is_staff=True)
self.ticket.assigned_to = staff_user
self.ticket.save()
email_1 = 'user1@mail.com'
ticketcc_1 = self.ticket.ticketcc_set.create(email=email_1)
# Add new email to CC
email_2 = 'user2@mail.com'
ticketcc_2 = self.ticket.add_email_to_ticketcc_if_not_in(email=email_2)
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
# Add existing email, doesn't change anything
self.ticket.add_email_to_ticketcc_if_not_in(email=email_1)
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
# Add mail from assigned user, doesn't change anything
self.ticket.add_email_to_ticketcc_if_not_in(email=staff_email)
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
self.ticket.add_email_to_ticketcc_if_not_in(user=staff_user)
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
# Move a ticketCC from ticket 1 to ticket 2
ticket_2 = Ticket.objects.create(queue=self.ticket.queue, title='Ticket 2', submitter_email=email_2)
self.assertEqual(ticket_2.ticketcc_set.count(), 0)
ticket_2.add_email_to_ticketcc_if_not_in(ticketcc=ticketcc_1)
self.assertEqual(ticketcc_1.ticket, ticket_2)
self.assertEqual(ticket_2.ticketcc_set.count(), 1)
# Adding email_2 doesn't change since it is already submitter email
ticket_2.add_email_to_ticketcc_if_not_in(email=email_2)
self.assertEqual(ticket_2.ticketcc_set.get(), ticketcc_1)
ticket_2.add_email_to_ticketcc_if_not_in(ticketcc=ticketcc_2)
self.assertEqual(ticket_2.ticketcc_set.get(), ticketcc_1)
# Finally test function raises a Value error when no parameter is given
self.assertRaises(ValueError, ticket_2.add_email_to_ticketcc_if_not_in)

View File

@ -52,6 +52,10 @@ urlpatterns = [
staff.mass_update,
name='mass_update'),
url(r'^tickets/merge$',
staff.merge_tickets,
name='merge_tickets'),
url(r'^tickets/(?P<ticket_id>[0-9]+)/$',
staff.view_ticket,
name='view'),

View File

@ -18,7 +18,7 @@ from django.core.exceptions import ValidationError, PermissionDenied
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.db.models import Q
from django.http import HttpResponseRedirect, Http404, HttpResponse, JsonResponse
from django.shortcuts import render, get_object_or_404
from django.shortcuts import render, get_object_or_404, redirect
from django.utils.translation import ugettext as _
from django.utils.html import escape
from django.utils import timezone
@ -38,7 +38,7 @@ from helpdesk.decorators import (
)
from helpdesk.forms import (
TicketForm, UserSettingsForm, EmailIgnoreForm, EditTicketForm, TicketCCForm,
TicketCCEmailForm, TicketCCUserForm, EditFollowUpForm, TicketDependencyForm
TicketCCEmailForm, TicketCCUserForm, EditFollowUpForm, TicketDependencyForm, MultipleTicketSelectForm
)
from helpdesk.decorators import superuser_required
from helpdesk.lib import (
@ -48,7 +48,7 @@ from helpdesk.lib import (
)
from helpdesk.models import (
Ticket, Queue, FollowUp, TicketChange, PreSetReply, FollowUpAttachment, SavedSearch,
IgnoreEmail, TicketCC, TicketDependency, UserSettings, KBItem,
IgnoreEmail, TicketCC, TicketDependency, UserSettings, KBItem, CustomField, TicketCustomFieldValue,
)
from helpdesk import settings as helpdesk_settings
import helpdesk.views.abstract_views as abstract_views
@ -61,6 +61,7 @@ from rest_framework.decorators import api_view
from datetime import date, datetime, timedelta
import re
from ..templated_email import send_templated_mail
User = get_user_model()
Query = get_query_class()
@ -766,6 +767,11 @@ def mass_update(request):
elif action == 'take':
user = request.user
action = 'assign'
elif action == 'merge':
# Redirect to the Merge View with selected tickets id in the GET request
return redirect(
reverse('helpdesk:merge_tickets') + '?' + '&'.join(['tickets=%s' % ticket_id for ticket_id in tickets])
)
huser = HelpdeskUser(request.user)
for t in Ticket.objects.filter(id__in=tickets):
@ -854,6 +860,151 @@ def mass_update(request):
mass_update = staff_member_required(mass_update)
# Prepare ticket attributes which will be displayed in the table to choose which value to keep when merging
ticket_attributes = (
('created', _('Created date')),
('due_date', _('Due on')),
('get_status_display', _('Status')),
('submitter_email', _('Submitter email')),
('assigned_to', _('Owner')),
('description', _('Description')),
('resolution', _('Resolution')),
)
@staff_member_required
def merge_tickets(request):
"""
An intermediate view to merge up to 3 tickets in one main ticket.
The user has to first select which ticket will receive the other tickets information and can also choose which
data to keep per attributes as well as custom fields.
Follow-ups and ticketCC will be moved to the main ticket and other tickets won't be able to receive new answers.
"""
ticket_select_form = MultipleTicketSelectForm(request.GET or None)
tickets = custom_fields = None
if ticket_select_form.is_valid():
tickets = ticket_select_form.cleaned_data.get('tickets')
custom_fields = CustomField.objects.all()
default = _('Not defined')
for ticket in tickets:
ticket.values = {}
# Prepare the value for each attributes of this ticket
for attribute, display_name in ticket_attributes:
value = getattr(ticket, attribute, default)
# Check if attr is a get_FIELD_display
if attribute.startswith('get_') and attribute.endswith('_display'):
# Hack to call methods like get_FIELD_display()
value = getattr(ticket, attribute, default)()
ticket.values[attribute] = {
'value': value,
'checked': str(ticket.id) == request.POST.get(attribute)
}
# Prepare the value for each custom fields of this ticket
for custom_field in custom_fields:
try:
value = ticket.ticketcustomfieldvalue_set.get(field=custom_field).value
except (TicketCustomFieldValue.DoesNotExist, ValueError):
value = default
ticket.values[custom_field.name] = {
'value': value,
'checked': str(ticket.id) == request.POST.get(custom_field.name)
}
if request.method == 'POST':
# Find which ticket has been chosen to be the main one
try:
chosen_ticket = tickets.get(id=request.POST.get('chosen_ticket'))
except Ticket.DoesNotExist:
ticket_select_form.add_error(
field='tickets',
error=_('Please choose a ticket in which the others will be merged into.')
)
else:
# Save ticket fields values
for attribute, display_name in ticket_attributes:
id_for_attribute = request.POST.get(attribute)
if id_for_attribute != chosen_ticket.id:
try:
selected_ticket = tickets.get(id=id_for_attribute)
except (Ticket.DoesNotExist, ValueError):
continue
# Check if attr is a get_FIELD_display
if attribute.startswith('get_') and attribute.endswith('_display'):
# Keep only the FIELD part
attribute = attribute[4:-8]
# Get value from selected ticket and then save it on the chosen ticket
value = getattr(selected_ticket, attribute)
setattr(chosen_ticket, attribute, value)
# Save custom fields values
for custom_field in custom_fields:
id_for_custom_field = request.POST.get(custom_field.name)
if id_for_custom_field != chosen_ticket.id:
try:
selected_ticket = tickets.get(id=id_for_custom_field)
except (Ticket.DoesNotExist, ValueError):
continue
# Check if the value for this ticket custom field exists
try:
value = selected_ticket.ticketcustomfieldvalue_set.get(field=custom_field).value
except TicketCustomFieldValue.DoesNotExist:
continue
# Create the custom field value or update it with the value from the selected ticket
custom_field_value, created = chosen_ticket.ticketcustomfieldvalue_set.get_or_create(
field=custom_field,
defaults={'value': value}
)
if not created:
custom_field_value.value = value
custom_field_value.save(update_fields=['value'])
# Save changes
chosen_ticket.save()
# For other tickets, save the link to the ticket in which they have been merged to
# and set status to DUPLICATE
for ticket in tickets.exclude(id=chosen_ticket.id):
ticket.merged_to = chosen_ticket
ticket.status = Ticket.DUPLICATE_STATUS
ticket.save()
# Send mail to submitter email and ticket CC to let them know ticket has been merged
context = safe_template_context(ticket)
if ticket.submitter_email:
send_templated_mail(
template_name='merged',
context=context,
recipients=[ticket.submitter_email],
bcc=[cc.email_address for cc in ticket.ticketcc_set.select_related('user')],
sender=ticket.queue.from_address,
fail_silently=True
)
# Move all followups and update their title to know they come from another ticket
ticket.followup_set.update(
ticket=chosen_ticket,
# Next might exceed maximum 200 characters limit
title=_('[Merged from #%(id)d] %(title)s') % {'id': ticket.id, 'title': ticket.title}
)
# Add submitter_email, assigned_to email and ticketcc to chosen ticket if necessary
chosen_ticket.add_email_to_ticketcc_if_not_in(email=ticket.submitter_email)
if ticket.assigned_to and ticket.assigned_to.email:
chosen_ticket.add_email_to_ticketcc_if_not_in(email=ticket.assigned_to.email)
for ticketcc in ticket.ticketcc_set.all():
chosen_ticket.add_email_to_ticketcc_if_not_in(ticketcc=ticketcc)
return redirect(chosen_ticket)
return render(request, 'helpdesk/ticket_merge.html', {
'tickets': tickets,
'ticket_attributes': ticket_attributes,
'custom_fields': custom_fields,
'ticket_select_form': ticket_select_form
})
@helpdesk_staff_member_required
def ticket_list(request):
context = {}