diff --git a/helpdesk/admin.py b/helpdesk/admin.py index d51cf209..f5dc76af 100644 --- a/helpdesk/admin.py +++ b/helpdesk/admin.py @@ -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) diff --git a/helpdesk/forms.py b/helpdesk/forms.py index 8a6781d2..f9abe18c 100644 --- a/helpdesk/forms.py +++ b/helpdesk/forms.py @@ -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'}) diff --git a/helpdesk/migrations/0038_checklist_checklisttemplate_checklisttask.py b/helpdesk/migrations/0038_checklist_checklisttemplate_checklisttask.py new file mode 100644 index 00000000..a3c80616 --- /dev/null +++ b/helpdesk/migrations/0038_checklist_checklisttemplate_checklisttask.py @@ -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',), + }, + ), + ] diff --git a/helpdesk/models.py b/helpdesk/models.py index 27151171..eeca54e2 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -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 diff --git a/helpdesk/static/helpdesk/helpdesk-extend.css b/helpdesk/static/helpdesk/helpdesk-extend.css index cdd6f2f0..f36f08d9 100644 --- a/helpdesk/static/helpdesk/helpdesk-extend.css +++ b/helpdesk/static/helpdesk/helpdesk-extend.css @@ -102,3 +102,7 @@ table .tickettitle { overflow: hidden; text-overflow: ellipsis; } + +.handle { + cursor: grab; +} diff --git a/helpdesk/templates/helpdesk/checklist_confirm_delete.html b/helpdesk/templates/helpdesk/checklist_confirm_delete.html new file mode 100644 index 00000000..1e5a697f --- /dev/null +++ b/helpdesk/templates/helpdesk/checklist_confirm_delete.html @@ -0,0 +1,45 @@ +{% extends "helpdesk/base.html" %} + +{% load i18n %} + +{% block helpdesk_title %}{% trans "Delete Checklist" %}{% endblock %} + +{% block helpdesk_breadcrumb %} + + + +{% endblock %} + +{% block helpdesk_body %} +
+
+
+
+

+ {% trans "Delete Checklist" %} +

+
+
+
+ {% csrf_token %} +

{% trans "Are you sure your want to delete checklist" %} {{ checklist.name }} ?

+
+ + + {% trans "Don't Delete" %} + + +
+
+
+
+
+
+{% endblock %} diff --git a/helpdesk/templates/helpdesk/checklist_form.html b/helpdesk/templates/helpdesk/checklist_form.html new file mode 100644 index 00000000..1b1026b8 --- /dev/null +++ b/helpdesk/templates/helpdesk/checklist_form.html @@ -0,0 +1,103 @@ +{% extends "helpdesk/base.html" %} + +{% load i18n %} + +{% block helpdesk_title %}{% trans "Edit Checklist" %}{% endblock %} + +{% block helpdesk_breadcrumb %} + + + +{% endblock %} + +{% block helpdesk_body %} +
+
+

+ {% trans "Edit Checklist" %} + + + {% trans "Delete checklist" %} + +

+
+
+ {% if form.non_field_errors %} +

+ {{ form.non_field_errors }} +

+ {% endif %} +
+ {% csrf_token %} +
+
+ {{ form.name.label_tag }} + {{ form.name }} + {{ form.name.errors }} +
+
+

Tasks

+ {{ formset.management_form }} + + + + + + + + + + {% for form in formset %} + {% include 'helpdesk/include/task_form_row.html' %} + {% endfor %} + +
{% trans "Position" %}{% trans "Description" %}{% trans "Delete?" %}
+ +
+ + + {% trans "Cancel Changes" %} + + +
+
+
+
+{% endblock %} + +{% block helpdesk_js %} + + + +{% endblock %} diff --git a/helpdesk/templates/helpdesk/checklist_template_confirm_delete.html b/helpdesk/templates/helpdesk/checklist_template_confirm_delete.html new file mode 100644 index 00000000..d1da22bb --- /dev/null +++ b/helpdesk/templates/helpdesk/checklist_template_confirm_delete.html @@ -0,0 +1,47 @@ +{% extends "helpdesk/base.html" %} + +{% load i18n %} + +{% block helpdesk_title %}{% trans "Delete Checklist Template" %}{% endblock %} + +{% block helpdesk_breadcrumb %} + + + +{% endblock %} + +{% block helpdesk_body %} +
+
+
+
+

+ {% trans "Delete Checklist Template" %} +

+
+
+
+ {% csrf_token %} +

{% trans "Are you sure your want to delete checklist template" %} {{ checklist_template.name }} ?

+
+ + + {% trans "Don't Delete" %} + + +
+
+
+
+
+
+{% endblock %} diff --git a/helpdesk/templates/helpdesk/checklist_templates.html b/helpdesk/templates/helpdesk/checklist_templates.html new file mode 100644 index 00000000..b8d2c6e1 --- /dev/null +++ b/helpdesk/templates/helpdesk/checklist_templates.html @@ -0,0 +1,119 @@ +{% extends "helpdesk/base.html" %} + +{% load i18n %} + +{% block helpdesk_title %}{% trans "Checklist Templates" %}{% endblock %} + +{% block helpdesk_breadcrumb %} + + +{% endblock %} + +{% block helpdesk_body %} +

{% trans "Maintain checklist templates" %}

+
+
+
+
+ {% if checklist_template %} + {% trans "Edit checklist template" %} + {% else %} + {% trans "Create new checklist template" %} + {% endif %} +
+
+
+ {% csrf_token %} + {{ form.as_p }} + + + + + + + + + + {% 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 %} + +
TaskActions
+ +
+ +
+
+
+
+
+ {% for checklist in checklists %} +
+ + {{ checklist.name }} + {% if checklist_template.id != checklist.id %} + + + + {% endif %} + + + + + {{ checklist.task_list|length }} {% trans "tasks" %} +
+ {% endfor %} +
+
+
+{% endblock %} + +{% block helpdesk_js %} + + + +{% endblock %} \ No newline at end of file diff --git a/helpdesk/templates/helpdesk/email_ignore_list.html b/helpdesk/templates/helpdesk/email_ignore_list.html index 3dda68b7..ad03e8b1 100644 --- a/helpdesk/templates/helpdesk/email_ignore_list.html +++ b/helpdesk/templates/helpdesk/email_ignore_list.html @@ -2,6 +2,13 @@ {% block helpdesk_title %}{% trans "Ignored E-Mail Addresses" %}{% endblock %} +{% block helpdesk_breadcrumb %} + + +{% endblock %} + {% block helpdesk_body %}{% blocktrans %}

Ignored E-Mail Addresses

diff --git a/helpdesk/templates/helpdesk/include/task_form_row.html b/helpdesk/templates/helpdesk/include/task_form_row.html new file mode 100644 index 00000000..bfe9f3da --- /dev/null +++ b/helpdesk/templates/helpdesk/include/task_form_row.html @@ -0,0 +1,16 @@ + + {{ form.id }} + + + {{ form.position }} + {{ form.position.errors }} + + + {{ form.description }} + {{ form.description.errors }} + + + {{ form.DELETE }} + {{ form.DELETE.errors }} + + \ No newline at end of file diff --git a/helpdesk/templates/helpdesk/include/template_task_form_row.html b/helpdesk/templates/helpdesk/include/template_task_form_row.html new file mode 100644 index 00000000..77a3af75 --- /dev/null +++ b/helpdesk/templates/helpdesk/include/template_task_form_row.html @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/helpdesk/templates/helpdesk/system_settings.html b/helpdesk/templates/helpdesk/system_settings.html index ad41f2b6..b09f86bd 100644 --- a/helpdesk/templates/helpdesk/system_settings.html +++ b/helpdesk/templates/helpdesk/system_settings.html @@ -18,6 +18,7 @@