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 helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.models import (
|
||||
Checklist,
|
||||
ChecklistTask,
|
||||
ChecklistTemplate,
|
||||
CustomField,
|
||||
EmailTemplate,
|
||||
EscalationExclusion,
|
||||
@ -41,6 +44,7 @@ class TicketAdmin(admin.ModelAdmin):
|
||||
'hidden_submitter_email', 'time_spent')
|
||||
date_hierarchy = 'created'
|
||||
list_filter = ('queue', 'assigned_to', 'status')
|
||||
search_fields = ('id', 'title')
|
||||
|
||||
def hidden_submitter_email(self, ticket):
|
||||
if ticket.submitter_email:
|
||||
@ -115,5 +119,24 @@ class IgnoreEmailAdmin(admin.ModelAdmin):
|
||||
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(EscalationExclusion)
|
||||
|
@ -7,7 +7,6 @@ forms.py - Definitions of newforms-based forms for creating and maintaining
|
||||
tickets.
|
||||
"""
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
from django import forms
|
||||
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.lib import convert_value, process_attachments, safe_template_context
|
||||
from helpdesk.models import (
|
||||
Checklist,
|
||||
ChecklistTemplate,
|
||||
CustomField,
|
||||
FollowUp,
|
||||
IgnoreEmail,
|
||||
@ -602,3 +603,46 @@ class MultipleTicketSelectForm(forms.Form):
|
||||
raise ValidationError(
|
||||
_('All selected tickets must share the same queue in order to be merged.'))
|
||||
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):
|
||||
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;
|
||||
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_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 %}
|
||||
<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>
|
||||
<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_presetreply_changelist' %}'>{% trans "Maintain Pre-Set Replies" %}</a></li>
|
||||
{% 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_head %}
|
||||
{% endblock %}
|
||||
|
||||
{% block h1_title %}{{ ticket.ticket_for_url }}{% endblock %}
|
||||
|
||||
{% block helpdesk_breadcrumb %}
|
||||
@ -166,7 +163,7 @@
|
||||
{% endif %}
|
||||
</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;'>
|
||||
|
||||
@ -188,7 +185,41 @@
|
||||
|
||||
</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;'>
|
||||
|
||||
@ -214,6 +245,33 @@
|
||||
|
||||
</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 %}
|
||||
{% endblock %}
|
||||
|
||||
@ -229,13 +287,15 @@ $( function() {
|
||||
$(document).ready(function() {
|
||||
$("#ShowFurtherEditOptions").click(function() {
|
||||
$("#FurtherEditOptions").toggle();
|
||||
return false;
|
||||
});
|
||||
|
||||
$("#ShowChecklistEditOptions").click(function() {
|
||||
$("#checklistEdit").toggle();
|
||||
});
|
||||
|
||||
$("#ShowFileUpload").click(function() {
|
||||
$("#FileUpload").fadeIn();
|
||||
$("#ShowFileUploadPara").hide();
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#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();
|
||||
|
||||
// lists for file input change events, then updates the associated text label
|
||||
|
@ -10,130 +10,254 @@
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-border">
|
||||
<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 %}
|
||||
<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:delete' ticket.id %}" class="ticket-delete"><button class="btn btn-danger btn-sm"><i class="fas fa-trash-alt"></i> {% trans "Delete" %}</button></a>
|
||||
|
|
||||
{% if ticket.on_hold %}
|
||||
<form class="form-inline ticket-hold" method='post' action='unhold/'>
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-warning btn-sm" type='submit'><i class="fas fa-play"></i> {% trans "Unhold" %}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form class="form-inline ticket-hold" method='post' action='hold/'>
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-warning btn-sm" type='submit'><i class="fas fa-pause"></i> {% trans "Hold" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</span></th></tr>
|
||||
<a href="{% url 'helpdesk:edit' ticket.id %}" class="btn btn-warning btn-sm ticket-edit">
|
||||
<i class="fas fa-pencil-alt"></i> {% trans "Edit" %}
|
||||
</a>
|
||||
| <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/'>
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-warning btn-sm" type='submit'>
|
||||
<i class="fas fa-play"></i> {% trans "Unhold" %}
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form class="form-inline ticket-hold" method='post' action='hold/'>
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-warning btn-sm" type='submit'>
|
||||
<i class="fas fa-pause"></i> {% trans "Hold" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for customfield in ticket.ticketcustomfieldvalue_set.all %}
|
||||
<tr>
|
||||
<th class="table-secondary">{{ customfield.field.label }}</th>
|
||||
<td>{% spaceless %}{% if "url" == customfield.field.data_type %}<a href='{{ customfield.value }}'>{{ customfield.value }}</a>
|
||||
{% elif "datetime" == customfield.field.data_type %}{{ customfield.value|datetime_string_format }}
|
||||
{% 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>
|
||||
<th class="table-secondary">{{ customfield.field.label }}</th>
|
||||
<td>
|
||||
{% spaceless %}
|
||||
{% if "url" == customfield.field.data_type %}
|
||||
<a href='{{ customfield.value }}'>{{ customfield.value }}</a>
|
||||
{% elif "datetime" == customfield.field.data_type %}
|
||||
{{ customfield.value|datetime_string_format }}
|
||||
{% 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>
|
||||
<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>
|
||||
<th class="table-active">{% trans "Submitted On" %}</th>
|
||||
<td>{{ ticket.created|date:"DATETIME_FORMAT" }} ({{ ticket.created|naturaltime }})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="table-active">{% trans "Assigned To" %}</th>
|
||||
<td>{{ ticket.get_assigned_to }}{% if _('Unassigned') == ticket.get_assigned_to %} <strong>
|
||||
<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>
|
||||
</strong>{% endif %}
|
||||
<td>
|
||||
{{ ticket.get_assigned_to }}
|
||||
{% 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>
|
||||
<th class="table-active">{% trans "Submitter E-Mail" %}</th>
|
||||
<td> {{ ticket.submitter_email }}
|
||||
{% 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 %}
|
||||
<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" %}'>
|
||||
<button type="button" class="btn btn-primary btn-sm"><i class="fas fa-search"></i></button></a></strong>
|
||||
<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." %}'>
|
||||
<button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-eye-slash"></i></button></a></strong>{% endif %}
|
||||
<td>
|
||||
{{ ticket.submitter_email }}
|
||||
{% if user.is_superuser %}
|
||||
{% if submitter_userprofile_url %}
|
||||
<a class="btn btn-primary btn-sm" data-toggle="tooltip" href='{{submitter_userprofile_url}}' title='{% trans "Edit " %}{{ ticket.submitter_email }}{% trans " user profile" %}'>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{% if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET != False and helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET != False %}
|
||||
<tr>
|
||||
{% if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET %}
|
||||
<th class="table-active">{% trans "Dependencies" %}</th>
|
||||
<td>
|
||||
<a data-toggle='tooltip' href='{% url 'helpdesk:ticket_dependency_add' ticket.id %}' title="{% trans "Click on 'Add Dependency', if you want to make this ticket dependent on another ticket. A ticket may not be closed until all tickets it depends on are closed." %}"><button type="button" class="btn btn-primary btn-sm float-right"><i class="fas fa-link"></i></button></a>
|
||||
{% for dep in ticket.ticketdependency.all %}
|
||||
{% if forloop.first %}<p>{% trans "This ticket cannot be resolved until the following ticket(s) are resolved" %}</p><ul>{% endif %}
|
||||
<li><a href='{{ dep.depends_on.get_absolute_url }}'>{{ dep.depends_on.ticket }} {{ dep.depends_on.title }}</a> ({{ dep.depends_on.get_status_display }}) <a href='{% url 'helpdesk:ticket_dependency_del' ticket.id dep.id %}'><button type="button" class="btn btn-warning btn-sm"><i class="fas fa-trash"></i></button></a></li>
|
||||
{% if forloop.last %}</ul>{% endif %}
|
||||
{% empty %}
|
||||
{% trans "This ticket has no dependencies." %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% else %}
|
||||
<th class="table-active"></th>
|
||||
<td></td>
|
||||
{% endif %}
|
||||
{% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}
|
||||
<th class="table-active">{% trans "Total time spent" %}</th>
|
||||
<td>{{ ticket.time_spent_formated }}</td>
|
||||
{% else %}
|
||||
<th class="table-active"></th>
|
||||
<td></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
{% if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET %}
|
||||
<th class="table-active">{% trans "Dependencies" %}</th>
|
||||
<td>
|
||||
<a data-toggle='tooltip' href='{% url 'helpdesk:ticket_dependency_add' ticket.id %}' title="{% trans "Click on 'Add Dependency', if you want to make this ticket dependent on another ticket. A ticket may not be closed until all tickets it depends on are closed." %}"><button type="button" class="btn btn-primary btn-sm float-right"><i class="fas fa-link"></i></button></a>
|
||||
{% for dep in ticket.ticketdependency.all %}
|
||||
{% if forloop.first %}<p>{% trans "This ticket cannot be resolved until the following ticket(s) are resolved" %}</p><ul>{% endif %}
|
||||
<li><a href='{{ dep.depends_on.get_absolute_url }}'>{{ dep.depends_on.ticket }} {{ dep.depends_on.title }}</a> ({{ dep.depends_on.get_status_display }}) <a href='{% url 'helpdesk:ticket_dependency_del' ticket.id dep.id %}'><button type="button" class="btn btn-warning btn-sm"><i class="fas fa-trash"></i></button></a></li>
|
||||
{% if forloop.last %}</ul>{% endif %}
|
||||
{% empty %}
|
||||
{% trans "This ticket has no dependencies." %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% else %}
|
||||
<th class="table-active"></th>
|
||||
<td></td>
|
||||
{% endif %}
|
||||
{% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}
|
||||
<th class="table-active">{% trans "Total time spent" %}</th>
|
||||
<td>{{ ticket.time_spent_formated }}</td>
|
||||
{% else %}
|
||||
<th class="table-active"></th>
|
||||
<td></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if ticket.kbitem %}
|
||||
<tr>
|
||||
<th class="table-active">{% trans "Knowlegebase item" %}</th>
|
||||
<td> <a href ="{{ticket.kbitem.query_url}}"> {{ticket.kbitem}} </a> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="table-active">{% trans "Knowlegebase item" %}</th>
|
||||
<td> <a href ="{{ticket.kbitem.query_url}}"> {{ticket.kbitem}} </a> </td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th class="table-active">{% trans "Attachments" %}</th>
|
||||
<td colspan="3">
|
||||
<ul>
|
||||
{% for followup in ticket.followup_set.all %}
|
||||
{% for attachment in followup.followupattachment_set.all %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
{% for attachment in followup.followupattachment_set.all %}
|
||||
<li>
|
||||
<a href='{{ attachment.file.url }}'>
|
||||
{{ attachment.filename }}
|
||||
</a> ({{ attachment.mime_type }}, {{ attachment.size|filesizeformat }})
|
||||
{% if followup.user and request.user == followup.user %}
|
||||
<a class="btn btn-danger btn-sm" href='{% url 'helpdesk:attachment_del' ticket.id attachment.id %}'>
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id="ticket-description" colspan='4'>
|
||||
<h4>{% trans "Description" %}</h4>
|
||||
{{ ticket.get_markdown|urlizetrunc:50|num_to_link }}</td>
|
||||
</tr>
|
||||
|
||||
{% if ticket.resolution %}<tr>
|
||||
<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>
|
||||
<th class="table-active">{% trans "Checklists" %}</th>
|
||||
<td colspan="3">
|
||||
<div class="container-fluid">
|
||||
<div class="row align-items-baseline">
|
||||
{% for checklist in ticket.checklists.all %}
|
||||
<div class="col-sm-4 col-xs-12">
|
||||
<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>
|
||||
<td colspan='2'>{{ ticket.get_resolution_markdown|urlizetrunc:50|linebreaksbr }}</td>
|
||||
</tr>{% endif %}
|
||||
<td id="ticket-description" colspan='4'>
|
||||
<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>
|
||||
</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>
|
||||
<!-- /.table-responsive -->
|
||||
</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.sites.models import Site
|
||||
from django.core import mail
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.urls import reverse
|
||||
@ -38,6 +36,7 @@ class TicketActionsTestCase(TestCase):
|
||||
)
|
||||
|
||||
self.ticket_data = {
|
||||
'queue': self.queue_public,
|
||||
'title': 'Test Ticket',
|
||||
'description': 'Some Test Ticket',
|
||||
}
|
||||
@ -73,8 +72,7 @@ class TicketActionsTestCase(TestCase):
|
||||
self.loginUser()
|
||||
|
||||
"""Tests whether staff can delete tickets"""
|
||||
ticket_data = dict(queue=self.queue_public, **self.ticket_data)
|
||||
ticket = Ticket.objects.create(**ticket_data)
|
||||
ticket = Ticket.objects.create(**self.ticket_data)
|
||||
ticket_id = ticket.id
|
||||
|
||||
response = self.client.get(reverse('helpdesk:delete', kwargs={
|
||||
|
@ -93,6 +93,16 @@ urlpatterns = [
|
||||
staff.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"),
|
||||
path("rss/", staff.rss_list, name="rss_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/delete/<int:id>/",
|
||||
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(
|
||||
r"^datatables_ticket_list/(?P<query>{})$".format(base64_pattern),
|
||||
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
|
||||
renders all staff-facing views.
|
||||
"""
|
||||
|
||||
from ..lib import format_time_spent
|
||||
from ..templated_email import send_templated_mail
|
||||
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.paginator import EmptyPage, PageNotAnInteger, Paginator
|
||||
from django.db.models import Q
|
||||
from django.forms import HiddenInput, inlineformset_factory, TextInput
|
||||
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse, reverse_lazy
|
||||
@ -38,10 +40,14 @@ from helpdesk.decorators import (
|
||||
superuser_required
|
||||
)
|
||||
from helpdesk.forms import (
|
||||
ChecklistForm,
|
||||
ChecklistTemplateForm,
|
||||
CreateChecklistForm,
|
||||
CUSTOMFIELD_DATE_FORMAT,
|
||||
EditFollowUpForm,
|
||||
EditTicketForm,
|
||||
EmailIgnoreForm,
|
||||
FormControlDeleteFormSet,
|
||||
MultipleTicketSelectForm,
|
||||
TicketCCEmailForm,
|
||||
TicketCCForm,
|
||||
@ -52,6 +58,9 @@ from helpdesk.forms import (
|
||||
)
|
||||
from helpdesk.lib import process_attachments, queue_template_context, safe_template_context
|
||||
from helpdesk.models import (
|
||||
Checklist,
|
||||
ChecklistTask,
|
||||
ChecklistTemplate,
|
||||
CustomField,
|
||||
FollowUp,
|
||||
FollowUpAttachment,
|
||||
@ -406,6 +415,19 @@ def view_ticket(request, ticket_id):
|
||||
else:
|
||||
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', {
|
||||
'ticket': ticket,
|
||||
'submitter_userprofile_url': submitter_userprofile_url,
|
||||
@ -416,6 +438,56 @@ def view_ticket(request, ticket_id):
|
||||
Q(queues=ticket.queue) | Q(queues__isnull=True)),
|
||||
'ticketcc_string': ticketcc_string,
|
||||
'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))
|
||||
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)
|
||||
# NOTE: jQuery's default for dates is mm/dd/yy
|
||||
# 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([
|
||||
not request.FILES,
|
||||
not comment,
|
||||
not changes_in_checklists,
|
||||
new_status == ticket.status,
|
||||
title == ticket.title,
|
||||
priority == int(ticket.priority),
|
||||
@ -785,6 +866,30 @@ def update_ticket(request, ticket_id, public=False):
|
||||
c.save()
|
||||
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 (
|
||||
Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS
|
||||
) and (
|
||||
@ -2030,3 +2135,30 @@ def date_rel_to_today(today, offset):
|
||||
def sort_string(begin, end):
|
||||
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)
|
||||
|
||||
|
||||
@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