forked from extern/django-helpdesk
commit
1ce945467c
@ -3,6 +3,9 @@ from django.contrib import admin
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from helpdesk import settings as helpdesk_settings
|
from helpdesk import settings as helpdesk_settings
|
||||||
from helpdesk.models import (
|
from helpdesk.models import (
|
||||||
|
Checklist,
|
||||||
|
ChecklistTask,
|
||||||
|
ChecklistTemplate,
|
||||||
CustomField,
|
CustomField,
|
||||||
EmailTemplate,
|
EmailTemplate,
|
||||||
EscalationExclusion,
|
EscalationExclusion,
|
||||||
@ -41,6 +44,7 @@ class TicketAdmin(admin.ModelAdmin):
|
|||||||
'hidden_submitter_email', 'time_spent')
|
'hidden_submitter_email', 'time_spent')
|
||||||
date_hierarchy = 'created'
|
date_hierarchy = 'created'
|
||||||
list_filter = ('queue', 'assigned_to', 'status')
|
list_filter = ('queue', 'assigned_to', 'status')
|
||||||
|
search_fields = ('id', 'title')
|
||||||
|
|
||||||
def hidden_submitter_email(self, ticket):
|
def hidden_submitter_email(self, ticket):
|
||||||
if ticket.submitter_email:
|
if ticket.submitter_email:
|
||||||
@ -115,5 +119,24 @@ class IgnoreEmailAdmin(admin.ModelAdmin):
|
|||||||
list_display = ('name', 'queue_list', 'email_address', 'keep_in_mailbox')
|
list_display = ('name', 'queue_list', 'email_address', 'keep_in_mailbox')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ChecklistTemplate)
|
||||||
|
class ChecklistTemplateAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'task_list')
|
||||||
|
search_fields = ('name', 'task_list')
|
||||||
|
|
||||||
|
|
||||||
|
class ChecklistTaskInline(admin.TabularInline):
|
||||||
|
model = ChecklistTask
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Checklist)
|
||||||
|
class ChecklistAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'ticket')
|
||||||
|
search_fields = ('name', 'ticket__id', 'ticket__title')
|
||||||
|
autocomplete_fields = ('ticket',)
|
||||||
|
list_select_related = ('ticket',)
|
||||||
|
inlines = (ChecklistTaskInline,)
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(PreSetReply)
|
admin.site.register(PreSetReply)
|
||||||
admin.site.register(EscalationExclusion)
|
admin.site.register(EscalationExclusion)
|
||||||
|
@ -7,7 +7,6 @@ forms.py - Definitions of newforms-based forms for creating and maintaining
|
|||||||
tickets.
|
tickets.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -18,6 +17,8 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from helpdesk import settings as helpdesk_settings
|
from helpdesk import settings as helpdesk_settings
|
||||||
from helpdesk.lib import convert_value, process_attachments, safe_template_context
|
from helpdesk.lib import convert_value, process_attachments, safe_template_context
|
||||||
from helpdesk.models import (
|
from helpdesk.models import (
|
||||||
|
Checklist,
|
||||||
|
ChecklistTemplate,
|
||||||
CustomField,
|
CustomField,
|
||||||
FollowUp,
|
FollowUp,
|
||||||
IgnoreEmail,
|
IgnoreEmail,
|
||||||
@ -602,3 +603,46 @@ class MultipleTicketSelectForm(forms.Form):
|
|||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('All selected tickets must share the same queue in order to be merged.'))
|
_('All selected tickets must share the same queue in order to be merged.'))
|
||||||
return tickets
|
return tickets
|
||||||
|
|
||||||
|
|
||||||
|
class ChecklistTemplateForm(forms.ModelForm):
|
||||||
|
name = forms.CharField(
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
task_list = forms.JSONField(widget=forms.HiddenInput())
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ChecklistTemplate
|
||||||
|
fields = ('name', 'task_list')
|
||||||
|
|
||||||
|
def clean_task_list(self):
|
||||||
|
task_list = self.cleaned_data['task_list']
|
||||||
|
return list(map(lambda task: task.strip(), task_list))
|
||||||
|
|
||||||
|
|
||||||
|
class ChecklistForm(forms.ModelForm):
|
||||||
|
name = forms.CharField(
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Checklist
|
||||||
|
fields = ('name',)
|
||||||
|
|
||||||
|
|
||||||
|
class CreateChecklistForm(ChecklistForm):
|
||||||
|
checklist_template = forms.ModelChoiceField(
|
||||||
|
label=_("Template"),
|
||||||
|
queryset=ChecklistTemplate.objects.all(),
|
||||||
|
widget=forms.Select(attrs={'class': 'form-control'}),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(ChecklistForm.Meta):
|
||||||
|
fields = ('checklist_template', 'name')
|
||||||
|
|
||||||
|
|
||||||
|
class FormControlDeleteFormSet(forms.BaseInlineFormSet):
|
||||||
|
deletion_widget = forms.CheckboxInput(attrs={'class': 'form-control'})
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
# Generated by Django 4.2 on 2023-04-28 21:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import helpdesk.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('helpdesk', '0037_alter_queue_email_box_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Checklist',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='Name')),
|
||||||
|
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to='helpdesk.ticket', verbose_name='Ticket')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Checklist',
|
||||||
|
'verbose_name_plural': 'Checklists',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ChecklistTemplate',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='Name')),
|
||||||
|
('task_list', models.JSONField(validators=[helpdesk.models.is_a_list_without_empty_element], verbose_name='Task List')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Checklist Template',
|
||||||
|
'verbose_name_plural': 'Checklist Templates',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ChecklistTask',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('description', models.CharField(max_length=250, verbose_name='Description')),
|
||||||
|
('completion_date', models.DateTimeField(blank=True, null=True, verbose_name='Completion Date')),
|
||||||
|
('position', models.PositiveSmallIntegerField(db_index=True, verbose_name='Position')),
|
||||||
|
('checklist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='helpdesk.checklist', verbose_name='Checklist')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Checklist Task',
|
||||||
|
'verbose_name_plural': 'Checklist Tasks',
|
||||||
|
'ordering': ('position',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -2002,3 +2002,92 @@ class TicketDependency(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s / %s' % (self.ticket, self.depends_on)
|
return '%s / %s' % (self.ticket, self.depends_on)
|
||||||
|
|
||||||
|
|
||||||
|
def is_a_list_without_empty_element(task_list):
|
||||||
|
if not isinstance(task_list, list):
|
||||||
|
raise ValidationError(f'{task_list} is not a list')
|
||||||
|
for task in task_list:
|
||||||
|
if not isinstance(task, str):
|
||||||
|
raise ValidationError(f'{task} is not a string')
|
||||||
|
if task.strip() == '':
|
||||||
|
raise ValidationError('A task cannot be an empty string')
|
||||||
|
|
||||||
|
|
||||||
|
class ChecklistTemplate(models.Model):
|
||||||
|
name = models.CharField(
|
||||||
|
verbose_name=_('Name'),
|
||||||
|
max_length=100
|
||||||
|
)
|
||||||
|
task_list = models.JSONField(verbose_name=_('Task List'), validators=[is_a_list_without_empty_element])
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Checklist Template')
|
||||||
|
verbose_name_plural = _('Checklist Templates')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Checklist(models.Model):
|
||||||
|
ticket = models.ForeignKey(
|
||||||
|
Ticket,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name=_('Ticket'),
|
||||||
|
related_name='checklists',
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
verbose_name=_('Name'),
|
||||||
|
max_length=100
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Checklist')
|
||||||
|
verbose_name_plural = _('Checklists')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def create_tasks_from_template(self, template):
|
||||||
|
for position, task in enumerate(template.task_list):
|
||||||
|
self.tasks.create(description=task, position=position)
|
||||||
|
|
||||||
|
|
||||||
|
class ChecklistTaskQuerySet(models.QuerySet):
|
||||||
|
def todo(self):
|
||||||
|
return self.filter(completion_date__isnull=True)
|
||||||
|
|
||||||
|
def completed(self):
|
||||||
|
return self.filter(completion_date__isnull=False)
|
||||||
|
|
||||||
|
|
||||||
|
class ChecklistTask(models.Model):
|
||||||
|
checklist = models.ForeignKey(
|
||||||
|
Checklist,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name=_('Checklist'),
|
||||||
|
related_name='tasks',
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
verbose_name=_('Description'),
|
||||||
|
max_length=250
|
||||||
|
)
|
||||||
|
completion_date = models.DateTimeField(
|
||||||
|
verbose_name=_('Completion Date'),
|
||||||
|
null=True,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
position = models.PositiveSmallIntegerField(
|
||||||
|
verbose_name=_('Position'),
|
||||||
|
db_index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = ChecklistTaskQuerySet.as_manager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Checklist Task')
|
||||||
|
verbose_name_plural = _('Checklist Tasks')
|
||||||
|
ordering = ('position',)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.description
|
||||||
|
@ -102,3 +102,7 @@ table .tickettitle {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
45
helpdesk/templates/helpdesk/checklist_confirm_delete.html
Normal file
45
helpdesk/templates/helpdesk/checklist_confirm_delete.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{% extends "helpdesk/base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block helpdesk_title %}{% trans "Delete Checklist" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block helpdesk_breadcrumb %}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'helpdesk:list' %}">{% trans "Tickets" %}</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'helpdesk:list' %}{{ ticket.id }}/">{{ ticket.queue.slug }}-{{ ticket.id }}</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active">{{ checklist.name }}</li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block helpdesk_body %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-6 offset-sm-3 col-xs-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>
|
||||||
|
{% trans "Delete Checklist" %}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method='post'>
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>{% trans "Are you sure your want to delete checklist" %} {{ checklist.name }} ?</p>
|
||||||
|
<div class='buttons form-group text-center'>
|
||||||
|
<a class="btn btn-secondary" href='{{ ticket.get_absolute_url }}'>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
{% trans "Don't Delete" %}
|
||||||
|
</a>
|
||||||
|
<button type='submit' class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
{% trans "Yes, I Understand - Delete" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
103
helpdesk/templates/helpdesk/checklist_form.html
Normal file
103
helpdesk/templates/helpdesk/checklist_form.html
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
{% extends "helpdesk/base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block helpdesk_title %}{% trans "Edit Checklist" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block helpdesk_breadcrumb %}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'helpdesk:list' %}">{% trans "Tickets" %}</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'helpdesk:list' %}{{ ticket.id }}/">{{ ticket.queue.slug }}-{{ ticket.id }}</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active">{{ checklist.name }}</li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block helpdesk_body %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>
|
||||||
|
{% trans "Edit Checklist" %}
|
||||||
|
<a class="btn btn-danger float-right" href='{% url "helpdesk:delete_ticket_checklist" ticket.id checklist.id %}'>
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
{% trans "Delete checklist" %}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<p class="text-danger">
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<form method='post'>
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-6 col-xs-12 form-group">
|
||||||
|
{{ form.name.label_tag }}
|
||||||
|
{{ form.name }}
|
||||||
|
{{ form.name.errors }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3>Tasks</h3>
|
||||||
|
{{ formset.management_form }}
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-right">{% trans "Position" %}</th>
|
||||||
|
<th>{% trans "Description" %}</th>
|
||||||
|
<th class="text-center">{% trans "Delete?" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tasks">
|
||||||
|
{% for form in formset %}
|
||||||
|
{% include 'helpdesk/include/task_form_row.html' %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button type="button" class="btn btn-secondary" id="addRow">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
{% trans "Add task" %}
|
||||||
|
</button>
|
||||||
|
<div class='buttons form-group text-center'>
|
||||||
|
<a class="btn btn-link" href='{{ ticket.get_absolute_url }}'>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
{% trans "Cancel Changes" %}
|
||||||
|
</a>
|
||||||
|
<button type='submit' class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
{% trans "Save Changes" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block helpdesk_js %}
|
||||||
|
<script src="https://unpkg.com/sortablejs-make/Sortable.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jquery-sortablejs@latest/jquery-sortable.js"></script>
|
||||||
|
<script>
|
||||||
|
const updatePosition = () => {
|
||||||
|
$('#tasks tr').each((index, taskRow) => {
|
||||||
|
$(taskRow).find('input[id$=position]').val(index + 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
const idTasksTotalForms = $('#id_tasks-TOTAL_FORMS')
|
||||||
|
$('#addRow').on('click', function() {
|
||||||
|
const formCount = idTasksTotalForms.val()
|
||||||
|
$('#tasks').append(`{% include 'helpdesk/include/task_form_row.html' with form=formset.empty_form %}`.replace(/__prefix__/g, formCount))
|
||||||
|
idTasksTotalForms.val(parseInt(formCount) + 1);
|
||||||
|
updatePosition()
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#tasks').sortable({
|
||||||
|
handle: '.handle',
|
||||||
|
onChange: updatePosition
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,47 @@
|
|||||||
|
{% extends "helpdesk/base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block helpdesk_title %}{% trans "Delete Checklist Template" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block helpdesk_breadcrumb %}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'helpdesk:system_settings' %}">{% trans "System Settings" %}</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'helpdesk:checklist_templates' %}">{% trans "Checklist Templates" %}</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active">
|
||||||
|
{{ checklist_template }}
|
||||||
|
</li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block helpdesk_body %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-6 offset-sm-3 col-xs-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>
|
||||||
|
{% trans "Delete Checklist Template" %}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method='post'>
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>{% trans "Are you sure your want to delete checklist template" %} {{ checklist_template.name }} ?</p>
|
||||||
|
<div class='buttons form-group text-center'>
|
||||||
|
<a class="btn btn-secondary" href='{% url 'helpdesk:checklist_templates' %}'>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
{% trans "Don't Delete" %}
|
||||||
|
</a>
|
||||||
|
<button type='submit' class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
{% trans "Yes, I Understand - Delete" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
119
helpdesk/templates/helpdesk/checklist_templates.html
Normal file
119
helpdesk/templates/helpdesk/checklist_templates.html
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
{% extends "helpdesk/base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block helpdesk_title %}{% trans "Checklist Templates" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block helpdesk_breadcrumb %}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'helpdesk:system_settings' %}">{% trans "System Settings" %}</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active">{% trans "Checklist Templates" %}</li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block helpdesk_body %}
|
||||||
|
<h2>{% trans "Maintain checklist templates" %}</h2>
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-sm-6 col-xs-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
{% if checklist_template %}
|
||||||
|
{% trans "Edit checklist template" %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Create new checklist template" %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<form method="post">
|
||||||
|
<div class="card-body">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Task</th>
|
||||||
|
<th class="text-center">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tasks">
|
||||||
|
{% if checklist_template %}
|
||||||
|
{% for value in checklist_template.task_list %}
|
||||||
|
{% include 'helpdesk/include/template_task_form_row.html' %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{% include 'helpdesk/include/template_task_form_row.html' %}
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button type="button" id="addTask" class="btn btn-sm btn-secondary">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
{% trans "Add another task" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
{% if checklist_template %}
|
||||||
|
<a href="{% url 'helpdesk:checklist_templates' %}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
{% trans "Cancel" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
{% trans "Save checklist template" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 col-xs-12">
|
||||||
|
<div class="list-group">
|
||||||
|
{% for checklist in checklists %}
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center{% if checklist_template.id == checklist.id %} active{% endif %}">
|
||||||
|
<span>
|
||||||
|
{{ checklist.name }}
|
||||||
|
{% if checklist_template.id != checklist.id %}
|
||||||
|
<a class="btn btn-secondary btn-sm" href="{% url 'helpdesk:edit_checklist_template' checklist.id %}">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a class="btn btn-sm btn-danger" href="{% url 'helpdesk:delete_checklist_template' checklist.id %}">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span class="badge badge-secondary badge-pill">{{ checklist.task_list|length }} {% trans "tasks" %}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block helpdesk_js %}
|
||||||
|
<script src="https://unpkg.com/sortablejs-make/Sortable.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jquery-sortablejs@latest/jquery-sortable.js"></script>
|
||||||
|
<script>
|
||||||
|
const updateTemplateTaskList = () => {
|
||||||
|
let tasks = []
|
||||||
|
$('#tasks .taskInput').each((index, taskInput) => {
|
||||||
|
tasks.push($(taskInput).val())
|
||||||
|
})
|
||||||
|
$('#id_task_list').val(JSON.stringify(tasks))
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeTask = (btn) => {
|
||||||
|
$(btn).parents('tr').remove()
|
||||||
|
updateTemplateTaskList()
|
||||||
|
}
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
$('#addTask').on('click', () => {
|
||||||
|
$('#tasks').append(`{% include 'helpdesk/include/template_task_form_row.html' %}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#tasks').sortable({
|
||||||
|
handle: '.handle',
|
||||||
|
onChange: updateTemplateTaskList
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
{% block helpdesk_title %}{% trans "Ignored E-Mail Addresses" %}{% endblock %}
|
{% block helpdesk_title %}{% trans "Ignored E-Mail Addresses" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block helpdesk_breadcrumb %}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'helpdesk:system_settings' %}">{% trans "System Settings" %}</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active">Ignored E-Mail Addresses</li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block helpdesk_body %}{% blocktrans %}
|
{% block helpdesk_body %}{% blocktrans %}
|
||||||
<h2>Ignored E-Mail Addresses</h2>
|
<h2>Ignored E-Mail Addresses</h2>
|
||||||
|
|
||||||
|
16
helpdesk/templates/helpdesk/include/task_form_row.html
Normal file
16
helpdesk/templates/helpdesk/include/task_form_row.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<tr>
|
||||||
|
{{ form.id }}
|
||||||
|
<td title="Drag & Drop" class="text-right handle">
|
||||||
|
<i class="fas fa-grip-vertical"></i>
|
||||||
|
{{ form.position }}
|
||||||
|
{{ form.position.errors }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ form.description }}
|
||||||
|
{{ form.description.errors }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ form.DELETE }}
|
||||||
|
{{ form.DELETE.errors }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
@ -0,0 +1,13 @@
|
|||||||
|
<tr>
|
||||||
|
<td title="Drag & Drop" class="text-center handle">
|
||||||
|
<i class="fas fa-grip-vertical"></i>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input class="form-control taskInput" oninput="updateTemplateTaskList()" required value="{{ value }}">
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<button type="button" class="btn btn-sm btn-danger" onclick="removeTask(this)">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href='{% url 'helpdesk:email_ignore' %}'>{% trans "E-Mail Ignore list" %}</a></li>
|
<li><a href='{% url 'helpdesk:email_ignore' %}'>{% trans "E-Mail Ignore list" %}</a></li>
|
||||||
|
<li><a href='{% url 'helpdesk:checklist_templates' %}'>{% trans "Checklist Templates" %}</a></li>
|
||||||
<li><a href='{% url 'admin:helpdesk_queue_changelist' %}'>{% trans "Maintain Queues" %}</a></li>
|
<li><a href='{% url 'admin:helpdesk_queue_changelist' %}'>{% trans "Maintain Queues" %}</a></li>
|
||||||
<li><a href='{% url 'admin:helpdesk_presetreply_changelist' %}'>{% trans "Maintain Pre-Set Replies" %}</a></li>
|
<li><a href='{% url 'admin:helpdesk_presetreply_changelist' %}'>{% trans "Maintain Pre-Set Replies" %}</a></li>
|
||||||
{% if helpdesk_settings.HELPDESK_KB_ENABLED %}
|
{% if helpdesk_settings.HELPDESK_KB_ENABLED %}
|
||||||
|
@ -6,9 +6,6 @@
|
|||||||
|
|
||||||
{% block helpdesk_title %}{{ ticket.queue.slug }}-{{ ticket.id }} : {% trans "View Ticket Details" %}{% endblock %}
|
{% block helpdesk_title %}{{ ticket.queue.slug }}-{{ ticket.id }} : {% trans "View Ticket Details" %}{% endblock %}
|
||||||
|
|
||||||
{% block helpdesk_head %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block h1_title %}{{ ticket.ticket_for_url }}{% endblock %}
|
{% block h1_title %}{{ ticket.ticket_for_url }}{% endblock %}
|
||||||
|
|
||||||
{% block helpdesk_breadcrumb %}
|
{% block helpdesk_breadcrumb %}
|
||||||
@ -166,7 +163,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<p id='ShowFurtherOptPara'><button class="btn btn-warning btn-sm" id='ShowFurtherEditOptions'>{% trans "Change Further Details »" %}</button></p>
|
<p id='ShowFurtherOptPara'><button type="button" class="btn btn-warning btn-sm" id='ShowFurtherEditOptions'>{% trans "Change Further Details »" %}</button></p>
|
||||||
|
|
||||||
<div id='FurtherEditOptions' style='display: none;'>
|
<div id='FurtherEditOptions' style='display: none;'>
|
||||||
|
|
||||||
@ -188,7 +185,41 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p id='ShowFileUploadPara'><button class="btn btn-warning btn-sm" id='ShowFileUpload'>{% trans "Attach File(s) »" %}</button></p>
|
{% if ticket.checklists.exists %}
|
||||||
|
<p>
|
||||||
|
<button type="button" class="btn btn-warning btn-sm" id='ShowChecklistEditOptions'>
|
||||||
|
{% trans "Update checklists" %} »
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id="checklistEdit" style="display: none">
|
||||||
|
<div class="row">
|
||||||
|
{% for checklist in ticket.checklists.all %}
|
||||||
|
<div class="col-sm-4 col-xs-12">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>{{ checklist }}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="list-group">
|
||||||
|
{% for task in checklist.tasks.all %}
|
||||||
|
<div class="list-group-item"{% if task.completion_date %} title="{% trans "Completed on" %} {{ task.completion_date }}" {% endif %}>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="checklist-{{ checklist.id }}" value="{{ task.id }}" {% if task.completion_date %} checked{% endif %}>
|
||||||
|
{{ task }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p id='ShowFileUploadPara'><button type="button" class="btn btn-warning btn-sm" id='ShowFileUpload'>{% trans "Attach File(s) »" %}</button></p>
|
||||||
|
|
||||||
<div id='FileUpload' style='display: none;'>
|
<div id='FileUpload' style='display: none;'>
|
||||||
|
|
||||||
@ -214,6 +245,33 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="createChecklistModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalCenterTitle" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">{% trans "Add a new checklist" %}</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="{% trans 'Close' %}">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>{% trans "You can select a template to generate a checklist with a predefined set of tasks." %}</p>
|
||||||
|
<p>{% trans "Ignore it and only give a name to create an empty checklist." %}</p>
|
||||||
|
{{ checklist_form.as_p }}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<a class="btn btn-secondary" href="{% url 'helpdesk:checklist_templates' %}">
|
||||||
|
Manage templates
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary">{% trans "Add" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -229,13 +287,15 @@ $( function() {
|
|||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$("#ShowFurtherEditOptions").click(function() {
|
$("#ShowFurtherEditOptions").click(function() {
|
||||||
$("#FurtherEditOptions").toggle();
|
$("#FurtherEditOptions").toggle();
|
||||||
return false;
|
});
|
||||||
|
|
||||||
|
$("#ShowChecklistEditOptions").click(function() {
|
||||||
|
$("#checklistEdit").toggle();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#ShowFileUpload").click(function() {
|
$("#ShowFileUpload").click(function() {
|
||||||
$("#FileUpload").fadeIn();
|
$("#FileUpload").fadeIn();
|
||||||
$("#ShowFileUploadPara").hide();
|
$("#ShowFileUploadPara").hide();
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#id_preset').change(function() {
|
$('#id_preset').change(function() {
|
||||||
@ -247,6 +307,19 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Preset name of checklist when a template is selected
|
||||||
|
$('#id_checklist_template').on('change', function() {
|
||||||
|
const nameField = $('#id_name')
|
||||||
|
const selectedTemplate = $(this).children(':selected')
|
||||||
|
if (nameField.val() === '' && selectedTemplate.val()) {
|
||||||
|
nameField.val(selectedTemplate.text())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$('.disabledTask').on('click', () => {
|
||||||
|
alert('{% trans 'If you want to update state of checklist tasks, please do a Follow-Up response and click on "Update checklists"' %}')
|
||||||
|
})
|
||||||
|
|
||||||
$("[data-toggle=tooltip]").tooltip();
|
$("[data-toggle=tooltip]").tooltip();
|
||||||
|
|
||||||
// lists for file input change events, then updates the associated text label
|
// lists for file input change events, then updates the associated text label
|
||||||
|
@ -10,64 +10,111 @@
|
|||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-border">
|
<table class="table table-sm table-border">
|
||||||
<thead class="thead-light">
|
<thead class="thead-light">
|
||||||
<tr class=''><th colspan='4'><h3>{{ ticket.queue.slug }}-{{ ticket.id }}. {{ ticket.title }} [{{ ticket.get_status }}]</h3>
|
<tr class=''>
|
||||||
|
<th colspan='4'>
|
||||||
|
<h3>{{ ticket.queue.slug }}-{{ ticket.id }}. {{ ticket.title }} [{{ ticket.get_status }}]</h3>
|
||||||
{% blocktrans with ticket.queue as queue %}Queue: {{ queue }}{% endblocktrans %}
|
{% blocktrans with ticket.queue as queue %}Queue: {{ queue }}{% endblocktrans %}
|
||||||
<span class='ticket_toolbar float-right'>
|
<span class='ticket_toolbar float-right'>
|
||||||
<a href="{% url 'helpdesk:edit' ticket.id %}" class="ticket-edit"><button class="btn btn-warning btn-sm"><i class="fas fa-pencil-alt"></i> {% trans "Edit" %}</button></a>
|
<a href="{% url 'helpdesk:edit' ticket.id %}" class="btn btn-warning btn-sm ticket-edit">
|
||||||
| <a href="{% url 'helpdesk:delete' ticket.id %}" class="ticket-delete"><button class="btn btn-danger btn-sm"><i class="fas fa-trash-alt"></i> {% trans "Delete" %}</button></a>
|
<i class="fas fa-pencil-alt"></i> {% trans "Edit" %}
|
||||||
|
|
</a>
|
||||||
{% if ticket.on_hold %}
|
| <a href="{% url 'helpdesk:delete' ticket.id %}" class="btn btn-danger btn-sm ticket-delete">
|
||||||
|
<i class="fas fa-trash-alt"></i> {% trans "Delete" %}
|
||||||
|
</a>
|
||||||
|
| {% if ticket.on_hold %}
|
||||||
<form class="form-inline ticket-hold" method='post' action='unhold/'>
|
<form class="form-inline ticket-hold" method='post' action='unhold/'>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="btn btn-warning btn-sm" type='submit'><i class="fas fa-play"></i> {% trans "Unhold" %}</button>
|
<button class="btn btn-warning btn-sm" type='submit'>
|
||||||
|
<i class="fas fa-play"></i> {% trans "Unhold" %}
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form class="form-inline ticket-hold" method='post' action='hold/'>
|
<form class="form-inline ticket-hold" method='post' action='hold/'>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="btn btn-warning btn-sm" type='submit'><i class="fas fa-pause"></i> {% trans "Hold" %}</button>
|
<button class="btn btn-warning btn-sm" type='submit'>
|
||||||
|
<i class="fas fa-pause"></i> {% trans "Hold" %}
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span></th></tr>
|
</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for customfield in ticket.ticketcustomfieldvalue_set.all %}
|
{% for customfield in ticket.ticketcustomfieldvalue_set.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="table-secondary">{{ customfield.field.label }}</th>
|
<th class="table-secondary">{{ customfield.field.label }}</th>
|
||||||
<td>{% spaceless %}{% if "url" == customfield.field.data_type %}<a href='{{ customfield.value }}'>{{ customfield.value }}</a>
|
<td>
|
||||||
{% elif "datetime" == customfield.field.data_type %}{{ customfield.value|datetime_string_format }}
|
{% spaceless %}
|
||||||
{% elif "date" == customfield.field.data_type %}{{ customfield.value|datetime_string_format }}
|
{% if "url" == customfield.field.data_type %}
|
||||||
{% elif "time" == customfield.field.data_type %}{{ customfield.value|datetime_string_format }}
|
<a href='{{ customfield.value }}'>{{ customfield.value }}</a>
|
||||||
{% else %}{{ customfield.value|default:"" }}
|
{% elif "datetime" == customfield.field.data_type %}
|
||||||
{% endif %}{% endspaceless %}</td>
|
{{ customfield.value|datetime_string_format }}
|
||||||
</tr>{% endfor %}
|
{% elif "date" == customfield.field.data_type %}
|
||||||
|
{{ customfield.value|datetime_string_format }}
|
||||||
|
{% elif "time" == customfield.field.data_type %}
|
||||||
|
{{ customfield.value|datetime_string_format }}
|
||||||
|
{% else %}
|
||||||
|
{{ customfield.value|default:"" }}
|
||||||
|
{% endif %}
|
||||||
|
{% endspaceless %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="table-active">{% trans "Due Date" %}</th>
|
<th class="table-active">{% trans "Due Date" %}</th>
|
||||||
<td>{{ ticket.due_date|date:"DATETIME_FORMAT" }} {% if ticket.due_date %}({{ ticket.due_date|naturaltime }}){% endif %}
|
<td>
|
||||||
|
{{ ticket.due_date|date:"DATETIME_FORMAT" }}
|
||||||
|
{% if ticket.due_date %}({{ ticket.due_date|naturaltime }}){% endif %}
|
||||||
</td>
|
</td>
|
||||||
<th class="table-active">{% trans "Submitted On" %}</th>
|
<th class="table-active">{% trans "Submitted On" %}</th>
|
||||||
<td>{{ ticket.created|date:"DATETIME_FORMAT" }} ({{ ticket.created|naturaltime }})</td>
|
<td>{{ ticket.created|date:"DATETIME_FORMAT" }} ({{ ticket.created|naturaltime }})</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="table-active">{% trans "Assigned To" %}</th>
|
<th class="table-active">{% trans "Assigned To" %}</th>
|
||||||
<td>{{ ticket.get_assigned_to }}{% if _('Unassigned') == ticket.get_assigned_to %} <strong>
|
<td>
|
||||||
<a data-toggle="tooltip" href='?take' title='{% trans "Assign this ticket to " %}{{ request.user.email }}'><button type="button" class="btn btn-primary btn-sm float-right"><i class="fas fa-hand-paper"></i></button></a>
|
{{ ticket.get_assigned_to }}
|
||||||
</strong>{% endif %}
|
{% if _('Unassigned') == ticket.get_assigned_to %}
|
||||||
|
<a class="btn btn-primary btn-sm float-right" data-toggle="tooltip" href='?take' title='{% trans "Assign this ticket to " %}{{ request.user.email }}'>
|
||||||
|
<i class="fas fa-hand-paper"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<th class="table-active">{% trans "Submitter E-Mail" %}</th>
|
<th class="table-active">{% trans "Submitter E-Mail" %}</th>
|
||||||
<td> {{ ticket.submitter_email }}
|
<td>
|
||||||
{% if user.is_superuser %} {% if submitter_userprofile_url %}<strong><a data-toggle="tooltip" href='{{submitter_userprofile_url}}' title='{% trans "Edit " %}{{ ticket.submitter_email }}{% trans " user profile" %}'><button type="button" class="btn btn-primary btn-sm"><i class="fas fa-address-book"></i></button></a></strong>{% endif %}
|
{{ ticket.submitter_email }}
|
||||||
<strong><a data-toggle="tooltip" href ="{% url 'helpdesk:list'%}?q={{ticket.submitter_email}}" title='{% trans "Display tickets filtered for " %}{{ ticket.submitter_email }}{% trans " as a keyword" %}'>
|
{% if user.is_superuser %}
|
||||||
<button type="button" class="btn btn-primary btn-sm"><i class="fas fa-search"></i></button></a></strong>
|
{% if submitter_userprofile_url %}
|
||||||
<strong><a data-toggle="tooltip" href='{% url 'helpdesk:email_ignore_add' %}?email={{ ticket.submitter_email }}' title='{% trans "Add email address for the ticket system to ignore." %}'>
|
<a class="btn btn-primary btn-sm" data-toggle="tooltip" href='{{submitter_userprofile_url}}' title='{% trans "Edit " %}{{ ticket.submitter_email }}{% trans " user profile" %}'>
|
||||||
<button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-eye-slash"></i></button></a></strong>{% endif %}
|
<i class="fas fa-address-book"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a class="btn btn-primary btn-sm" data-toggle="tooltip" href ="{% url 'helpdesk:list'%}?q={{ticket.submitter_email}}" title='{% trans "Display tickets filtered for " %}{{ ticket.submitter_email }}{% trans " as a keyword" %}'>
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-warning btn-sm float-right" data-toggle="tooltip" href='{% url 'helpdesk:email_ignore_add' %}?email={{ ticket.submitter_email }}' title='{% trans "Add email address for the ticket system to ignore." %}'>
|
||||||
|
<i class="fas fa-eye-slash"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="table-active">{% trans "Priority" %}</th>
|
<th class="table-active">{% trans "Priority" %}</th>
|
||||||
<td class="{% if ticket.priority < 3 %}table-warning{% endif %}">{{ ticket.get_priority_display }}
|
<td class="{% if ticket.priority < 3 %}table-warning{% endif %}">
|
||||||
|
{{ ticket.get_priority_display }}
|
||||||
</td>
|
</td>
|
||||||
<th class="table-active">{% trans "Copies To" %}</th>
|
<th class="table-active">{% trans "Copies To" %}</th>
|
||||||
<td>{{ ticketcc_string }} <a data-toggle='tooltip' href='{% url 'helpdesk:ticket_cc' ticket.id %}' title='{% trans "Click here to add / remove people who should receive an e-mail whenever this ticket is updated." %}'><strong><button type="button" class="btn btn-warning btn-sm float-right"><i class="fa fa-share"></i></button></strong></a>{% if SHOW_SUBSCRIBE %} <strong><a data-toggle='tooltip' href='?subscribe' title='{% trans "Click here to subscribe yourself to this ticket, if you want to receive an e-mail whenever this ticket is updated." %}'><button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-rss-square"></i></button></a></strong>{% endif %}</td>
|
<td>
|
||||||
|
{{ ticketcc_string }}
|
||||||
|
<a class="btn btn-warning btn-sm float-right" data-toggle='tooltip' href='{% url 'helpdesk:ticket_cc' ticket.id %}' title='{% trans "Click here to add / remove people who should receive an e-mail whenever this ticket is updated." %}'>
|
||||||
|
<i class="fa fa-share"></i>
|
||||||
|
</a>
|
||||||
|
{% if SHOW_SUBSCRIBE %}
|
||||||
|
<a class="btn btn-warning btn-sm float-right" data-toggle='tooltip' href='?subscribe' title='{% trans "Click here to subscribe yourself to this ticket, if you want to receive an e-mail whenever this ticket is updated." %}'>
|
||||||
|
<i class="fas fa-rss-square"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{% if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET != False and helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET != False %}
|
{% if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET != False and helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET != False %}
|
||||||
@ -109,9 +156,14 @@
|
|||||||
<ul>
|
<ul>
|
||||||
{% for followup in ticket.followup_set.all %}
|
{% for followup in ticket.followup_set.all %}
|
||||||
{% for attachment in followup.followupattachment_set.all %}
|
{% for attachment in followup.followupattachment_set.all %}
|
||||||
<li><a href='{{ attachment.file.url }}'>{{ attachment.filename }}</a> ({{ attachment.mime_type }}, {{ attachment.size|filesizeformat }})
|
<li>
|
||||||
|
<a href='{{ attachment.file.url }}'>
|
||||||
|
{{ attachment.filename }}
|
||||||
|
</a> ({{ attachment.mime_type }}, {{ attachment.size|filesizeformat }})
|
||||||
{% if followup.user and request.user == followup.user %}
|
{% 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>
|
<a class="btn btn-danger btn-sm" href='{% url 'helpdesk:attachment_del' ticket.id attachment.id %}'>
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -120,20 +172,92 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td id="ticket-description" colspan='4'>
|
<th class="table-active">{% trans "Checklists" %}</th>
|
||||||
<h4>{% trans "Description" %}</h4>
|
<td colspan="3">
|
||||||
{{ ticket.get_markdown|urlizetrunc:50|num_to_link }}</td>
|
<div class="container-fluid">
|
||||||
</tr>
|
<div class="row align-items-baseline">
|
||||||
|
{% for checklist in ticket.checklists.all %}
|
||||||
{% if ticket.resolution %}<tr>
|
<div class="col-sm-4 col-xs-12">
|
||||||
<th colspan='2'>{% trans "Resolution" %}{% if "Resolved" == ticket.get_status_display %} <a href='?close'><button type="button" class="btn btn-warning btn-sm">{% trans "Accept and Close" %}</button></a>{% endif %}</th>
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>
|
||||||
|
<span data-toggle="collapse" data-target="#checklist{{ checklist.id }}" onclick="$(this).siblings('button').children('i').toggleClass('fa-caret-down fa-caret-up')">
|
||||||
|
{{ checklist }}
|
||||||
|
</span>
|
||||||
|
<a class="btn btn-link btn-sm" href="{% url 'helpdesk:edit_ticket_checklist' ticket.id checklist.id %}">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-secondary btn-sm float-right" data-toggle="collapse" data-target="#checklist{{ checklist.id }}" onclick="$(this).children('i').toggleClass('fa-caret-down fa-caret-up')">
|
||||||
|
<i class="fas fa-caret-down"></i>
|
||||||
|
</button>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body collapse" id="checklist{{ checklist.id }}">
|
||||||
|
<div class="list-group">
|
||||||
|
{% for task in checklist.tasks.all %}
|
||||||
|
<div class="list-group-item"{% if task.completion_date %} title="{% trans "Completed on" %} {{ task.completion_date }}" {% endif %}>
|
||||||
|
<label class="disabledTask">
|
||||||
|
<input type="checkbox" disabled{% if task.completion_date %} checked{% endif %}>
|
||||||
|
{{ task }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if checklist.tasks.completed.count %}
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="progress">
|
||||||
|
{% widthratio checklist.tasks.completed.count checklist.tasks.count 100 as width %}
|
||||||
|
<div class="progress-bar" role="progressbar" style="width: {{ width }}%" aria-valuenow="{{ width }}" aria-valuemin="0" aria-valuemax="100">
|
||||||
|
{{ width }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="col-sm-4 col-xs-12">
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" data-toggle="modal" data-target="#createChecklistModal">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
{% trans "Create new checklist" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan='2'>{{ ticket.get_resolution_markdown|urlizetrunc:50|linebreaksbr }}</td>
|
<td id="ticket-description" colspan='4'>
|
||||||
</tr>{% endif %}
|
<h4>{% trans "Description" %}</h4>
|
||||||
|
{{ ticket.get_markdown|urlizetrunc:50|num_to_link }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% if ticket.resolution %}
|
||||||
|
<tr>
|
||||||
|
<th colspan='4'>
|
||||||
|
{% trans "Resolution" %}
|
||||||
|
{% if "Resolved" == ticket.get_status_display %}
|
||||||
|
<a href='?close'>
|
||||||
|
<button type="button" class="btn btn-warning btn-sm">
|
||||||
|
{% trans "Accept and Close" %}
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan='4'>{{ ticket.get_resolution_markdown|urlizetrunc:50|linebreaksbr }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<a href='#FurtherEditOptions'><button type="button" class="btn btn-sm btn-warning float-right" onclick="$('#FurtherEditOptions').fadeIn()"><i class="fas fa-pencil-alt"></i> {% trans "Edit details" %}</button></a>
|
<a href='#FurtherEditOptions'>
|
||||||
|
<button type="button" class="btn btn-sm btn-warning float-right" onclick="$('#FurtherEditOptions').fadeIn()">
|
||||||
|
<i class="fas fa-pencil-alt"></i> {% trans "Edit details" %}
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- /.table-responsive -->
|
<!-- /.table-responsive -->
|
||||||
</div>
|
</div>
|
||||||
|
252
helpdesk/tests/test_checklist.py
Normal file
252
helpdesk/tests/test_checklist.py
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from helpdesk.models import Checklist, ChecklistTask, ChecklistTemplate, Queue, Ticket
|
||||||
|
|
||||||
|
|
||||||
|
class TicketChecklistTestCase(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
user = get_user_model().objects.create_user('User', password='pass')
|
||||||
|
user.is_staff = True
|
||||||
|
user.save()
|
||||||
|
cls.user = user
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.client.login(username='User', password='pass')
|
||||||
|
|
||||||
|
self.ticket = Ticket.objects.create(queue=Queue.objects.create(title='Queue', slug='queue'))
|
||||||
|
|
||||||
|
def test_create_checklist(self):
|
||||||
|
self.assertEqual(self.ticket.checklists.count(), 0)
|
||||||
|
checklist_name = 'test empty checklist'
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('helpdesk:view', kwargs={'ticket_id': self.ticket.id}),
|
||||||
|
data={'name': checklist_name},
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'helpdesk/checklist_form.html')
|
||||||
|
self.assertContains(response, checklist_name)
|
||||||
|
|
||||||
|
self.assertEqual(self.ticket.checklists.count(), 1)
|
||||||
|
|
||||||
|
def test_create_checklist_from_template(self):
|
||||||
|
self.assertEqual(self.ticket.checklists.count(), 0)
|
||||||
|
checklist_name = 'test checklist from template'
|
||||||
|
|
||||||
|
checklist_template = ChecklistTemplate.objects.create(
|
||||||
|
name='Test template',
|
||||||
|
task_list=['first', 'second', 'last']
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('helpdesk:view', kwargs={'ticket_id': self.ticket.id}),
|
||||||
|
data={'name': checklist_name, 'checklist_template': checklist_template.id},
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'helpdesk/checklist_form.html')
|
||||||
|
self.assertContains(response, checklist_name)
|
||||||
|
|
||||||
|
self.assertEqual(self.ticket.checklists.count(), 1)
|
||||||
|
created_checklist = self.ticket.checklists.get()
|
||||||
|
self.assertEqual(created_checklist.tasks.count(), 3)
|
||||||
|
self.assertEqual(created_checklist.tasks.all()[0].description, 'first')
|
||||||
|
self.assertEqual(created_checklist.tasks.all()[1].description, 'second')
|
||||||
|
self.assertEqual(created_checklist.tasks.all()[2].description, 'last')
|
||||||
|
|
||||||
|
def test_edit_checklist(self):
|
||||||
|
checklist = self.ticket.checklists.create(name='Test checklist')
|
||||||
|
first_task = checklist.tasks.create(description='First task', position=1)
|
||||||
|
checklist.tasks.create(description='To delete task', position=2)
|
||||||
|
|
||||||
|
url = reverse('helpdesk:edit_ticket_checklist', kwargs={
|
||||||
|
'ticket_id': self.ticket.id,
|
||||||
|
'checklist_id': checklist.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
response = self.client.get(url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'helpdesk/checklist_form.html')
|
||||||
|
self.assertContains(response, 'Test checklist')
|
||||||
|
self.assertContains(response, 'First task')
|
||||||
|
self.assertContains(response, 'To delete task')
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
'name': 'New name',
|
||||||
|
'tasks-TOTAL_FORMS': 3,
|
||||||
|
'tasks-INITIAL_FORMS': 2,
|
||||||
|
'tasks-0-id': '1',
|
||||||
|
'tasks-0-description': 'First task edited',
|
||||||
|
'tasks-0-position': '2',
|
||||||
|
'tasks-1-id': '2',
|
||||||
|
'tasks-1-description': 'To delete task',
|
||||||
|
'tasks-1-DELETE': 'on',
|
||||||
|
'tasks-1-position': '2',
|
||||||
|
'tasks-2-description': 'New first task',
|
||||||
|
'tasks-2-position': '1',
|
||||||
|
},
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'helpdesk/ticket.html')
|
||||||
|
|
||||||
|
checklist.refresh_from_db()
|
||||||
|
self.assertEqual(checklist.name, 'New name')
|
||||||
|
self.assertEqual(checklist.tasks.count(), 2)
|
||||||
|
first_task.refresh_from_db()
|
||||||
|
self.assertEqual(first_task.description, 'First task edited')
|
||||||
|
self.assertEqual(checklist.tasks.all()[0].description, 'New first task')
|
||||||
|
self.assertEqual(checklist.tasks.all()[1].description, 'First task edited')
|
||||||
|
|
||||||
|
def test_delete_checklist(self):
|
||||||
|
checklist = self.ticket.checklists.create(name='Test checklist')
|
||||||
|
checklist.tasks.create(description='First task', position=1)
|
||||||
|
self.assertEqual(Checklist.objects.count(), 1)
|
||||||
|
self.assertEqual(ChecklistTask.objects.count(), 1)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
'helpdesk:delete_ticket_checklist',
|
||||||
|
kwargs={'ticket_id': self.ticket.id, 'checklist_id': checklist.id}
|
||||||
|
),
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'helpdesk/ticket.html')
|
||||||
|
|
||||||
|
self.assertEqual(Checklist.objects.count(), 0)
|
||||||
|
self.assertEqual(ChecklistTask.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_mark_task_as_done(self):
|
||||||
|
checklist = self.ticket.checklists.create(name='Test checklist')
|
||||||
|
task = checklist.tasks.create(description='Task', position=1)
|
||||||
|
self.assertIsNone(task.completion_date)
|
||||||
|
|
||||||
|
self.assertEqual(self.ticket.followup_set.count(), 0)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('helpdesk:update', kwargs={'ticket_id': self.ticket.id}),
|
||||||
|
data={
|
||||||
|
f'checklist-{checklist.id}': task.id
|
||||||
|
},
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'helpdesk/ticket.html')
|
||||||
|
|
||||||
|
self.assertEqual(self.ticket.followup_set.count(), 1)
|
||||||
|
followup = self.ticket.followup_set.get()
|
||||||
|
self.assertEqual(followup.ticketchange_set.count(), 1)
|
||||||
|
self.assertEqual(followup.ticketchange_set.get().old_value, 'To do')
|
||||||
|
self.assertEqual(followup.ticketchange_set.get().new_value, 'Completed')
|
||||||
|
|
||||||
|
task.refresh_from_db()
|
||||||
|
self.assertIsNotNone(task.completion_date)
|
||||||
|
|
||||||
|
def test_mark_task_as_undone(self):
|
||||||
|
checklist = self.ticket.checklists.create(name='Test checklist')
|
||||||
|
task = checklist.tasks.create(description='Task', position=1, completion_date=datetime(2023, 5, 1))
|
||||||
|
self.assertIsNotNone(task.completion_date)
|
||||||
|
|
||||||
|
self.assertEqual(self.ticket.followup_set.count(), 0)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('helpdesk:update', kwargs={'ticket_id': self.ticket.id}),
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'helpdesk/ticket.html')
|
||||||
|
|
||||||
|
self.assertEqual(self.ticket.followup_set.count(), 1)
|
||||||
|
followup = self.ticket.followup_set.get()
|
||||||
|
self.assertEqual(followup.ticketchange_set.count(), 1)
|
||||||
|
self.assertEqual(followup.ticketchange_set.get().old_value, 'Completed')
|
||||||
|
self.assertEqual(followup.ticketchange_set.get().new_value, 'To do')
|
||||||
|
|
||||||
|
task.refresh_from_db()
|
||||||
|
self.assertIsNone(task.completion_date)
|
||||||
|
|
||||||
|
def test_display_checklist_templates(self):
|
||||||
|
ChecklistTemplate.objects.create(
|
||||||
|
name='Test checklist template',
|
||||||
|
task_list=['first', 'second', 'third']
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(reverse('helpdesk:checklist_templates'))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html')
|
||||||
|
self.assertContains(response, 'Test checklist template')
|
||||||
|
self.assertContains(response, '3 tasks')
|
||||||
|
|
||||||
|
def test_create_checklist_template(self):
|
||||||
|
self.assertEqual(ChecklistTemplate.objects.count(), 0)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('helpdesk:checklist_templates'),
|
||||||
|
data={
|
||||||
|
'name': 'Test checklist template',
|
||||||
|
'task_list': '["first", "second", "third"]'
|
||||||
|
},
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html')
|
||||||
|
|
||||||
|
self.assertEqual(ChecklistTemplate.objects.count(), 1)
|
||||||
|
checklist_template = ChecklistTemplate.objects.get()
|
||||||
|
self.assertEqual(checklist_template.name, 'Test checklist template')
|
||||||
|
self.assertEqual(checklist_template.task_list, ['first', 'second', 'third'])
|
||||||
|
|
||||||
|
def test_edit_checklist_template(self):
|
||||||
|
checklist_template = ChecklistTemplate.objects.create(
|
||||||
|
name='Test checklist template',
|
||||||
|
task_list=['first', 'second', 'third']
|
||||||
|
)
|
||||||
|
self.assertEqual(ChecklistTemplate.objects.count(), 1)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('helpdesk:edit_checklist_template', kwargs={'checklist_template_id': checklist_template.id}),
|
||||||
|
data={
|
||||||
|
'name': 'New checklist template',
|
||||||
|
'task_list': '["new first", "second", "third", "last"]'
|
||||||
|
},
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html')
|
||||||
|
|
||||||
|
self.assertEqual(ChecklistTemplate.objects.count(), 1)
|
||||||
|
checklist_template.refresh_from_db()
|
||||||
|
self.assertEqual(checklist_template.name, 'New checklist template')
|
||||||
|
self.assertEqual(checklist_template.task_list, ['new first', 'second', 'third', 'last'])
|
||||||
|
|
||||||
|
def test_delete_checklist_template(self):
|
||||||
|
checklist_template = ChecklistTemplate.objects.create(
|
||||||
|
name='Test checklist template',
|
||||||
|
task_list=['first', 'second', 'third']
|
||||||
|
)
|
||||||
|
self.assertEqual(ChecklistTemplate.objects.count(), 1)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('helpdesk:delete_checklist_template', kwargs={'checklist_template_id': checklist_template.id}),
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html')
|
||||||
|
|
||||||
|
self.assertEqual(ChecklistTemplate.objects.count(), 0)
|
@ -1,6 +1,4 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.sites.models import Site
|
|
||||||
from django.core import mail
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import Client
|
from django.test.client import Client
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -38,6 +36,7 @@ class TicketActionsTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.ticket_data = {
|
self.ticket_data = {
|
||||||
|
'queue': self.queue_public,
|
||||||
'title': 'Test Ticket',
|
'title': 'Test Ticket',
|
||||||
'description': 'Some Test Ticket',
|
'description': 'Some Test Ticket',
|
||||||
}
|
}
|
||||||
@ -73,8 +72,7 @@ class TicketActionsTestCase(TestCase):
|
|||||||
self.loginUser()
|
self.loginUser()
|
||||||
|
|
||||||
"""Tests whether staff can delete tickets"""
|
"""Tests whether staff can delete tickets"""
|
||||||
ticket_data = dict(queue=self.queue_public, **self.ticket_data)
|
ticket = Ticket.objects.create(**self.ticket_data)
|
||||||
ticket = Ticket.objects.create(**ticket_data)
|
|
||||||
ticket_id = ticket.id
|
ticket_id = ticket.id
|
||||||
|
|
||||||
response = self.client.get(reverse('helpdesk:delete', kwargs={
|
response = self.client.get(reverse('helpdesk:delete', kwargs={
|
||||||
|
@ -93,6 +93,16 @@ urlpatterns = [
|
|||||||
staff.attachment_del,
|
staff.attachment_del,
|
||||||
name="attachment_del",
|
name="attachment_del",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"tickets/<int:ticket_id>/checklists/<int:checklist_id>/",
|
||||||
|
staff.edit_ticket_checklist,
|
||||||
|
name="edit_ticket_checklist"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"tickets/<int:ticket_id>/checklists/<int:checklist_id>/delete/",
|
||||||
|
staff.delete_ticket_checklist,
|
||||||
|
name="delete_ticket_checklist"
|
||||||
|
),
|
||||||
re_path(r"^raw/(?P<type>\w+)/$", staff.raw_details, name="raw"),
|
re_path(r"^raw/(?P<type>\w+)/$", staff.raw_details, name="raw"),
|
||||||
path("rss/", staff.rss_list, name="rss_index"),
|
path("rss/", staff.rss_list, name="rss_index"),
|
||||||
path("reports/", staff.report_index, name="report_index"),
|
path("reports/", staff.report_index, name="report_index"),
|
||||||
@ -105,6 +115,17 @@ urlpatterns = [
|
|||||||
path("ignore/add/", staff.email_ignore_add, name="email_ignore_add"),
|
path("ignore/add/", staff.email_ignore_add, name="email_ignore_add"),
|
||||||
path("ignore/delete/<int:id>/",
|
path("ignore/delete/<int:id>/",
|
||||||
staff.email_ignore_del, name="email_ignore_del"),
|
staff.email_ignore_del, name="email_ignore_del"),
|
||||||
|
path("checklist-templates/", staff.checklist_templates, name="checklist_templates"),
|
||||||
|
path(
|
||||||
|
"checklist-templates/<int:checklist_template_id>/",
|
||||||
|
staff.checklist_templates,
|
||||||
|
name="edit_checklist_template"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"checklist-templates/<int:checklist_template_id>/delete/",
|
||||||
|
staff.delete_checklist_template,
|
||||||
|
name="delete_checklist_template"
|
||||||
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^datatables_ticket_list/(?P<query>{})$".format(base64_pattern),
|
r"^datatables_ticket_list/(?P<query>{})$".format(base64_pattern),
|
||||||
staff.datatables_ticket_list,
|
staff.datatables_ticket_list,
|
||||||
|
@ -6,6 +6,7 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
|
|||||||
views/staff.py - The bulk of the application - provides most business logic and
|
views/staff.py - The bulk of the application - provides most business logic and
|
||||||
renders all staff-facing views.
|
renders all staff-facing views.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from ..lib import format_time_spent
|
from ..lib import format_time_spent
|
||||||
from ..templated_email import send_templated_mail
|
from ..templated_email import send_templated_mail
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@ -20,6 +21,7 @@ from django.core.exceptions import PermissionDenied, ValidationError
|
|||||||
from django.core.handlers.wsgi import WSGIRequest
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.forms import HiddenInput, inlineformset_factory, TextInput
|
||||||
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
|
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
@ -38,10 +40,14 @@ from helpdesk.decorators import (
|
|||||||
superuser_required
|
superuser_required
|
||||||
)
|
)
|
||||||
from helpdesk.forms import (
|
from helpdesk.forms import (
|
||||||
|
ChecklistForm,
|
||||||
|
ChecklistTemplateForm,
|
||||||
|
CreateChecklistForm,
|
||||||
CUSTOMFIELD_DATE_FORMAT,
|
CUSTOMFIELD_DATE_FORMAT,
|
||||||
EditFollowUpForm,
|
EditFollowUpForm,
|
||||||
EditTicketForm,
|
EditTicketForm,
|
||||||
EmailIgnoreForm,
|
EmailIgnoreForm,
|
||||||
|
FormControlDeleteFormSet,
|
||||||
MultipleTicketSelectForm,
|
MultipleTicketSelectForm,
|
||||||
TicketCCEmailForm,
|
TicketCCEmailForm,
|
||||||
TicketCCForm,
|
TicketCCForm,
|
||||||
@ -52,6 +58,9 @@ from helpdesk.forms import (
|
|||||||
)
|
)
|
||||||
from helpdesk.lib import process_attachments, queue_template_context, safe_template_context
|
from helpdesk.lib import process_attachments, queue_template_context, safe_template_context
|
||||||
from helpdesk.models import (
|
from helpdesk.models import (
|
||||||
|
Checklist,
|
||||||
|
ChecklistTask,
|
||||||
|
ChecklistTemplate,
|
||||||
CustomField,
|
CustomField,
|
||||||
FollowUp,
|
FollowUp,
|
||||||
FollowUpAttachment,
|
FollowUpAttachment,
|
||||||
@ -406,6 +415,19 @@ def view_ticket(request, ticket_id):
|
|||||||
else:
|
else:
|
||||||
submitter_userprofile_url = None
|
submitter_userprofile_url = None
|
||||||
|
|
||||||
|
checklist_form = CreateChecklistForm(request.POST or None)
|
||||||
|
if checklist_form.is_valid():
|
||||||
|
checklist = checklist_form.save(commit=False)
|
||||||
|
checklist.ticket = ticket
|
||||||
|
checklist.save()
|
||||||
|
|
||||||
|
checklist_template = checklist_form.cleaned_data.get('checklist_template')
|
||||||
|
# Add predefined tasks if template has been selected
|
||||||
|
if checklist_template:
|
||||||
|
checklist.create_tasks_from_template(checklist_template)
|
||||||
|
|
||||||
|
return redirect('helpdesk:edit_ticket_checklist', ticket.id, checklist.id)
|
||||||
|
|
||||||
return render(request, 'helpdesk/ticket.html', {
|
return render(request, 'helpdesk/ticket.html', {
|
||||||
'ticket': ticket,
|
'ticket': ticket,
|
||||||
'submitter_userprofile_url': submitter_userprofile_url,
|
'submitter_userprofile_url': submitter_userprofile_url,
|
||||||
@ -416,6 +438,56 @@ def view_ticket(request, ticket_id):
|
|||||||
Q(queues=ticket.queue) | Q(queues__isnull=True)),
|
Q(queues=ticket.queue) | Q(queues__isnull=True)),
|
||||||
'ticketcc_string': ticketcc_string,
|
'ticketcc_string': ticketcc_string,
|
||||||
'SHOW_SUBSCRIBE': show_subscribe,
|
'SHOW_SUBSCRIBE': show_subscribe,
|
||||||
|
'checklist_form': checklist_form,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@helpdesk_staff_member_required
|
||||||
|
def edit_ticket_checklist(request, ticket_id, checklist_id):
|
||||||
|
ticket = get_object_or_404(Ticket, id=ticket_id)
|
||||||
|
ticket_perm_check(request, ticket)
|
||||||
|
checklist = get_object_or_404(ticket.checklists.all(), id=checklist_id)
|
||||||
|
|
||||||
|
form = ChecklistForm(request.POST or None, instance=checklist)
|
||||||
|
TaskFormSet = inlineformset_factory(
|
||||||
|
Checklist,
|
||||||
|
ChecklistTask,
|
||||||
|
formset=FormControlDeleteFormSet,
|
||||||
|
fields=['description', 'position'],
|
||||||
|
widgets={
|
||||||
|
'position': HiddenInput(),
|
||||||
|
'description': TextInput(attrs={'class': 'form-control'}),
|
||||||
|
},
|
||||||
|
can_delete=True,
|
||||||
|
extra=0
|
||||||
|
)
|
||||||
|
formset = TaskFormSet(request.POST or None, instance=checklist)
|
||||||
|
if form.is_valid() and formset.is_valid():
|
||||||
|
form.save()
|
||||||
|
formset.save()
|
||||||
|
return redirect(ticket)
|
||||||
|
|
||||||
|
return render(request, 'helpdesk/checklist_form.html', {
|
||||||
|
'ticket': ticket,
|
||||||
|
'checklist': checklist,
|
||||||
|
'form': form,
|
||||||
|
'formset': formset,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@helpdesk_staff_member_required
|
||||||
|
def delete_ticket_checklist(request, ticket_id, checklist_id):
|
||||||
|
ticket = get_object_or_404(Ticket, id=ticket_id)
|
||||||
|
ticket_perm_check(request, ticket)
|
||||||
|
checklist = get_object_or_404(ticket.checklists.all(), id=checklist_id)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
checklist.delete()
|
||||||
|
return redirect(ticket)
|
||||||
|
|
||||||
|
return render(request, 'helpdesk/checklist_confirm_delete.html', {
|
||||||
|
'ticket': ticket,
|
||||||
|
'checklist': checklist,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -669,6 +741,14 @@ def update_ticket(request, ticket_id, public=False):
|
|||||||
owner = int(request.POST.get('owner', -1))
|
owner = int(request.POST.get('owner', -1))
|
||||||
priority = int(request.POST.get('priority', ticket.priority))
|
priority = int(request.POST.get('priority', ticket.priority))
|
||||||
|
|
||||||
|
# Check if a change happened on checklists
|
||||||
|
changes_in_checklists = False
|
||||||
|
for checklist in ticket.checklists.all():
|
||||||
|
old_completed_id = sorted(list(checklist.tasks.completed().values_list('id', flat=True)))
|
||||||
|
new_completed_id = sorted(list(map(int, request.POST.getlist(f'checklist-{checklist.id}', []))))
|
||||||
|
if old_completed_id != new_completed_id:
|
||||||
|
changes_in_checklists = True
|
||||||
|
|
||||||
time_spent = get_time_spent_from_request(request)
|
time_spent = get_time_spent_from_request(request)
|
||||||
# NOTE: jQuery's default for dates is mm/dd/yy
|
# NOTE: jQuery's default for dates is mm/dd/yy
|
||||||
# very US-centric but for now that's the only format supported
|
# very US-centric but for now that's the only format supported
|
||||||
@ -677,6 +757,7 @@ def update_ticket(request, ticket_id, public=False):
|
|||||||
no_changes = all([
|
no_changes = all([
|
||||||
not request.FILES,
|
not request.FILES,
|
||||||
not comment,
|
not comment,
|
||||||
|
not changes_in_checklists,
|
||||||
new_status == ticket.status,
|
new_status == ticket.status,
|
||||||
title == ticket.title,
|
title == ticket.title,
|
||||||
priority == int(ticket.priority),
|
priority == int(ticket.priority),
|
||||||
@ -785,6 +866,30 @@ def update_ticket(request, ticket_id, public=False):
|
|||||||
c.save()
|
c.save()
|
||||||
ticket.due_date = due_date
|
ticket.due_date = due_date
|
||||||
|
|
||||||
|
if changes_in_checklists:
|
||||||
|
for checklist in ticket.checklists.all():
|
||||||
|
new_completed_tasks = list(map(int, request.POST.getlist(f'checklist-{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 (
|
if new_status in (
|
||||||
Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS
|
Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS
|
||||||
) and (
|
) and (
|
||||||
@ -2030,3 +2135,30 @@ def date_rel_to_today(today, offset):
|
|||||||
def sort_string(begin, end):
|
def sort_string(begin, end):
|
||||||
return 'sort=created&date_from=%s&date_to=%s&status=%s&status=%s&status=%s' % (
|
return 'sort=created&date_from=%s&date_to=%s&status=%s&status=%s&status=%s' % (
|
||||||
begin, end, Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS, Ticket.RESOLVED_STATUS)
|
begin, end, Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS, Ticket.RESOLVED_STATUS)
|
||||||
|
|
||||||
|
|
||||||
|
@helpdesk_staff_member_required
|
||||||
|
def checklist_templates(request, checklist_template_id=None):
|
||||||
|
checklist_template = None
|
||||||
|
if checklist_template_id:
|
||||||
|
checklist_template = get_object_or_404(ChecklistTemplate, id=checklist_template_id)
|
||||||
|
form = ChecklistTemplateForm(request.POST or None, instance=checklist_template)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
return redirect('helpdesk:checklist_templates')
|
||||||
|
return render(request, 'helpdesk/checklist_templates.html', {
|
||||||
|
'checklists': ChecklistTemplate.objects.all(),
|
||||||
|
'checklist_template': checklist_template,
|
||||||
|
'form': form
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@helpdesk_staff_member_required
|
||||||
|
def delete_checklist_template(request, checklist_template_id):
|
||||||
|
checklist_template = get_object_or_404(ChecklistTemplate, id=checklist_template_id)
|
||||||
|
if request.method == 'POST':
|
||||||
|
checklist_template.delete()
|
||||||
|
return redirect('helpdesk:checklist_templates')
|
||||||
|
return render(request, 'helpdesk/checklist_template_confirm_delete.html', {
|
||||||
|
'checklist_template': checklist_template,
|
||||||
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user