From 9e9ebd45a76427738e8caa5123eb9ed3a969e5fd Mon Sep 17 00:00:00 2001 From: Benbb96 Date: Sat, 22 Apr 2023 22:55:41 +0200 Subject: [PATCH 01/14] Create models and admin for Checklist --- helpdesk/admin.py | 22 +++++- ...ecklist_checklisttemplate_checklisttask.py | 52 ++++++++++++++ helpdesk/models.py | 70 +++++++++++++++++++ 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 helpdesk/migrations/0038_checklist_checklisttemplate_checklisttask.py diff --git a/helpdesk/admin.py b/helpdesk/admin.py index d51cf209..c3b97cd5 100644 --- a/helpdesk/admin.py +++ b/helpdesk/admin.py @@ -13,7 +13,7 @@ from helpdesk.models import ( PreSetReply, Queue, Ticket, - TicketChange + TicketChange, Checklist, ChecklistTemplate, ChecklistTask ) @@ -41,6 +41,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 +116,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/migrations/0038_checklist_checklisttemplate_checklisttask.py b/helpdesk/migrations/0038_checklist_checklisttemplate_checklisttask.py new file mode 100644 index 00000000..3bfd0156 --- /dev/null +++ b/helpdesk/migrations/0038_checklist_checklisttemplate_checklisttask.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2 on 2023-04-22 20:50 + +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')), + ('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', + }, + ), + ] diff --git a/helpdesk/models.py b/helpdesk/models.py index 27151171..d72e62ca 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -2002,3 +2002,73 @@ 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 == '': + 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 + + +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 + ) + + class Meta: + verbose_name = _('Checklist Task') + verbose_name_plural = _('Checklist Tasks') + + def __str__(self): + return self.description From 9e7b4ef9bfafbcc368bab6cd36265fe04d7b5b49 Mon Sep 17 00:00:00 2001 From: Benbb96 Date: Sat, 22 Apr 2023 23:20:41 +0200 Subject: [PATCH 02/14] Reformat file ticket_desc_table.html --- .../templates/helpdesk/ticket_desc_table.html | 230 ++++++++++++------ 1 file changed, 149 insertions(+), 81 deletions(-) diff --git a/helpdesk/templates/helpdesk/ticket_desc_table.html b/helpdesk/templates/helpdesk/ticket_desc_table.html index 4a09a58b..ba980af5 100644 --- a/helpdesk/templates/helpdesk/ticket_desc_table.html +++ b/helpdesk/templates/helpdesk/ticket_desc_table.html @@ -10,111 +10,163 @@
- + + + {% trans "Edit" %} + + | + {% trans "Delete" %} + + | {% if ticket.on_hold %} + + {% csrf_token %} + + + {% else %} + + {% csrf_token %} + + + {% endif %} + + + {% for customfield in ticket.ticketcustomfieldvalue_set.all %} - - - - {% endfor %} + + + + + {% endfor %} - - - - - + {% 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 %} - - - {% else %} - - - {% endif %} - {% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %} - - - {% else %} - - - {% endif %} - + + {% if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET %} + + + {% else %} + + + {% endif %} + {% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %} + + + {% else %} + + + {% endif %} + {% endif %} {% if ticket.kbitem %} - - - - + + + + {% endif %} @@ -122,18 +174,34 @@ + {{ ticket.get_markdown|urlizetrunc:50|num_to_link }} + - {% if ticket.resolution %} - - - - - {% endif %} + {% if ticket.resolution %} + + + + + + + {% endif %}

{{ ticket.queue.slug }}-{{ ticket.id }}. {{ ticket.title }} [{{ ticket.get_status }}]

+
+

{{ ticket.queue.slug }}-{{ ticket.id }}. {{ ticket.title }} [{{ ticket.get_status }}]

{% blocktrans with ticket.queue as queue %}Queue: {{ queue }}{% endblocktrans %} - - | - | - {% if ticket.on_hold %} -
- {% csrf_token %} - -
- {% else %} -
- {% csrf_token %} - -
- {% endif %} -
{{ customfield.field.label }}{% spaceless %}{% if "url" == customfield.field.data_type %}{{ customfield.value }} - {% 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 %}
{{ customfield.field.label }} + {% spaceless %} + {% if "url" == customfield.field.data_type %} + {{ customfield.value }} + {% 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 %} +
{% trans "Due Date" %}{{ ticket.due_date|date:"DATETIME_FORMAT" }} {% if ticket.due_date %}({{ ticket.due_date|naturaltime }}){% endif %} + + {{ ticket.due_date|date:"DATETIME_FORMAT" }} + {% if ticket.due_date %}({{ ticket.due_date|naturaltime }}){% endif %} {% trans "Submitted On" %} {{ ticket.created|date:"DATETIME_FORMAT" }} ({{ ticket.created|naturaltime }})
{% trans "Assigned To" %}{{ ticket.get_assigned_to }}{% if _('Unassigned') == ticket.get_assigned_to %} - - {% endif %} + + {{ ticket.get_assigned_to }} + {% if _('Unassigned') == ticket.get_assigned_to %} + + + + {% endif %} {% trans "Submitter E-Mail" %} {{ ticket.submitter_email }} - {% if user.is_superuser %} {% if submitter_userprofile_url %}{% endif %} - - - - {% endif %} + + {{ ticket.submitter_email }} + {% if user.is_superuser %} + {% if submitter_userprofile_url %} + + + + {% endif %} + + + + + + + {% endif %}
{% trans "Priority" %}{{ ticket.get_priority_display }} + + {{ ticket.get_priority_display }} {% trans "Copies To" %}{{ ticketcc_string }} {% if SHOW_SUBSCRIBE %} {% endif %} + {{ ticketcc_string }} + + + + {% if SHOW_SUBSCRIBE %} + + + + {% endif %} +
{% trans "Dependencies" %} - - {% for dep in ticket.ticketdependency.all %} - {% if forloop.first %}

{% trans "This ticket cannot be resolved until the following ticket(s) are resolved" %}

{% endif %} - {% empty %} - {% trans "This ticket has no dependencies." %} - {% endfor %} -
{% trans "Total time spent" %}{{ ticket.time_spent_formated }}
{% trans "Dependencies" %} + + {% for dep in ticket.ticketdependency.all %} + {% if forloop.first %}

{% trans "This ticket cannot be resolved until the following ticket(s) are resolved" %}

{% endif %} + {% empty %} + {% trans "This ticket has no dependencies." %} + {% endfor %} +
{% trans "Total time spent" %}{{ ticket.time_spent_formated }}
{% trans "Knowlegebase item" %} {{ticket.kbitem}}
{% trans "Knowlegebase item" %} {{ticket.kbitem}}
{% trans "Attachments" %}
    {% for followup in ticket.followup_set.all %} - {% for attachment in followup.followupattachment_set.all %} -
  • {{ attachment.filename }} ({{ attachment.mime_type }}, {{ attachment.size|filesizeformat }}) - {% if followup.user and request.user == followup.user %} - - {% endif %} -
  • - {% endfor %} + {% for attachment in followup.followupattachment_set.all %} +
  • + + {{ attachment.filename }} + ({{ attachment.mime_type }}, {{ attachment.size|filesizeformat }}) + {% if followup.user and request.user == followup.user %} + + + + {% endif %} +
  • + {% endfor %} {% endfor %}

{% trans "Description" %}

- {{ ticket.get_markdown|urlizetrunc:50|num_to_link }}
{% trans "Resolution" %}{% if "Resolved" == ticket.get_status_display %} {% endif %}
{{ ticket.get_resolution_markdown|urlizetrunc:50|linebreaksbr }}
+ {% trans "Resolution" %} + {% if "Resolved" == ticket.get_status_display %} + + + + {% endif %} +
{{ ticket.get_resolution_markdown|urlizetrunc:50|linebreaksbr }}
- + + +
From 8be55fb7f2f0aa69f52e452fc44165ed9b638484 Mon Sep 17 00:00:00 2001 From: Benbb96 Date: Sat, 22 Apr 2023 23:56:23 +0200 Subject: [PATCH 03/14] Display checklists on ticket page --- helpdesk/models.py | 10 ++++ .../templates/helpdesk/ticket_desc_table.html | 56 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/helpdesk/models.py b/helpdesk/models.py index d72e62ca..ec3f68f4 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -2049,6 +2049,14 @@ class Checklist(models.Model): return self.name +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, @@ -2066,6 +2074,8 @@ class ChecklistTask(models.Model): blank=True ) + objects = ChecklistTaskQuerySet.as_manager() + class Meta: verbose_name = _('Checklist Task') verbose_name_plural = _('Checklist Tasks') diff --git a/helpdesk/templates/helpdesk/ticket_desc_table.html b/helpdesk/templates/helpdesk/ticket_desc_table.html index ba980af5..8aa85c04 100644 --- a/helpdesk/templates/helpdesk/ticket_desc_table.html +++ b/helpdesk/templates/helpdesk/ticket_desc_table.html @@ -171,6 +171,62 @@ + + {% trans "Checklists" %} + +
+
+ {% for checklist in ticket.checklists.all %} +
+
+
+

+ {{ checklist }} + +

+
+
+
+ {% for task in checklist.tasks.all %} +
+ + {{ task }} + {% if task.completion_date %} + + ({% trans "Completed on" %} {{ task.completion_date }}) + + {% endif %} +
+ {% endfor %} +
+
+ +
+
+ {% endfor %} +
+
+
+

Add a checklist

+
+
+ TODO +
+
+
+
+
+ +

{% trans "Description" %}

From b8d06a0fb1d25e4e0f0b284092b8afb61781cbaf Mon Sep 17 00:00:00 2001 From: Benbb96 Date: Sun, 23 Apr 2023 00:36:10 +0200 Subject: [PATCH 04/14] Show a form to create a new checklist, with the possibility to use a preset template --- helpdesk/forms.py | 27 ++++++++++++++++++- helpdesk/models.py | 6 +++++ .../templates/helpdesk/ticket_desc_table.html | 8 +++++- helpdesk/views/staff.py | 15 ++++++++++- 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/helpdesk/forms.py b/helpdesk/forms.py index 8a6781d2..0c6ac3d3 100644 --- a/helpdesk/forms.py +++ b/helpdesk/forms.py @@ -26,7 +26,9 @@ from helpdesk.models import ( TicketCC, TicketCustomFieldValue, TicketDependency, - UserSettings + UserSettings, + Checklist, + ChecklistTemplate ) from helpdesk.settings import ( CUSTOMFIELD_DATE_FORMAT, @@ -602,3 +604,26 @@ class MultipleTicketSelectForm(forms.Form): raise ValidationError( _('All selected tickets must share the same queue in order to be merged.')) return tickets + + +class ChecklistForm(forms.ModelForm): + checklist_template = forms.ModelChoiceField( + label=_("Template"), + queryset=ChecklistTemplate.objects.all(), + widget=forms.Select(attrs={'class': 'form-wontrol'}), + required=False, + ) + name = forms.CharField( + widget=forms.TextInput(attrs={'class': 'form-wontrol'}), + required=False, + ) + + class Meta: + model = Checklist + fields = ('name',) + + def clean(self): + if not self.cleaned_data.get('checklist_template') and not self.cleaned_data.get('name'): + raise ValidationError(_('Please choose at least a name or a template for the new checklist')) + if self.cleaned_data.get('checklist_template') and self.cleaned_data.get('name'): + raise ValidationError(_('Please choose either a name or a template for the new checklist')) diff --git a/helpdesk/models.py b/helpdesk/models.py index ec3f68f4..d090529d 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -2028,6 +2028,12 @@ class ChecklistTemplate(models.Model): def __str__(self): return self.name + def create_checklist_for_ticket(self, ticket): + checklist = ticket.checklists.create(name=self.name) + for task in self.task_list: + checklist.tasks.create(description=task) + return checklist + class Checklist(models.Model): ticket = models.ForeignKey( diff --git a/helpdesk/templates/helpdesk/ticket_desc_table.html b/helpdesk/templates/helpdesk/ticket_desc_table.html index 8aa85c04..36eba139 100644 --- a/helpdesk/templates/helpdesk/ticket_desc_table.html +++ b/helpdesk/templates/helpdesk/ticket_desc_table.html @@ -219,7 +219,13 @@

Add a checklist

- TODO +
+ {% csrf_token %} + {{ checklist_form.as_p }} + +
diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 934a9af9..40e8dbe1 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -48,7 +48,8 @@ from helpdesk.forms import ( TicketCCUserForm, TicketDependencyForm, TicketForm, - UserSettingsForm + UserSettingsForm, + ChecklistForm ) from helpdesk.lib import process_attachments, queue_template_context, safe_template_context from helpdesk.models import ( @@ -406,6 +407,17 @@ def view_ticket(request, ticket_id): else: submitter_userprofile_url = None + checklist_form = ChecklistForm(request.POST or None) + if checklist_form.is_valid(): + checklist_template = checklist_form.cleaned_data.get('checklist_template') + if checklist_template: + checklist_template.create_checklist_for_ticket(ticket) + else: + checklist = checklist_form.save(commit=False) + checklist.ticket = ticket + checklist.save() + return redirect(ticket) + return render(request, 'helpdesk/ticket.html', { 'ticket': ticket, 'submitter_userprofile_url': submitter_userprofile_url, @@ -416,6 +428,7 @@ 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, }) From af1ba5f2055f45f9b066f1e78f3275180f00d7e3 Mon Sep 17 00:00:00 2001 From: Benbb96 Date: Sat, 29 Apr 2023 00:24:06 +0200 Subject: [PATCH 05/14] Create a page to edit checklist name and tasks + another page for deletion --- helpdesk/forms.py | 27 +++-- ...ecklist_checklisttemplate_checklisttask.py | 4 +- helpdesk/models.py | 9 +- .../helpdesk/checklist_confirm_delete.html | 45 ++++++++ .../templates/helpdesk/checklist_form.html | 106 ++++++++++++++++++ helpdesk/templates/helpdesk/ticket.html | 5 + .../templates/helpdesk/ticket_desc_table.html | 17 +-- helpdesk/urls.py | 10 ++ helpdesk/views/staff.py | 60 +++++++++- 9 files changed, 262 insertions(+), 21 deletions(-) create mode 100644 helpdesk/templates/helpdesk/checklist_confirm_delete.html create mode 100644 helpdesk/templates/helpdesk/checklist_form.html diff --git a/helpdesk/forms.py b/helpdesk/forms.py index 0c6ac3d3..b9a00ee3 100644 --- a/helpdesk/forms.py +++ b/helpdesk/forms.py @@ -607,23 +607,34 @@ class MultipleTicketSelectForm(forms.Form): class ChecklistForm(forms.ModelForm): - checklist_template = forms.ModelChoiceField( - label=_("Template"), - queryset=ChecklistTemplate.objects.all(), - widget=forms.Select(attrs={'class': 'form-wontrol'}), - required=False, - ) name = forms.CharField( - widget=forms.TextInput(attrs={'class': 'form-wontrol'}), - required=False, + 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, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['name'].required = False + def clean(self): if not self.cleaned_data.get('checklist_template') and not self.cleaned_data.get('name'): raise ValidationError(_('Please choose at least a name or a template for the new checklist')) if self.cleaned_data.get('checklist_template') and self.cleaned_data.get('name'): raise ValidationError(_('Please choose either a name or a template for the new checklist')) + + +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 index 3bfd0156..a3c80616 100644 --- a/helpdesk/migrations/0038_checklist_checklisttemplate_checklisttask.py +++ b/helpdesk/migrations/0038_checklist_checklisttemplate_checklisttask.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2 on 2023-04-22 20:50 +# Generated by Django 4.2 on 2023-04-28 21:23 from django.db import migrations, models import django.db.models.deletion @@ -42,11 +42,13 @@ class Migration(migrations.Migration): ('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 d090529d..7f9e2072 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -2030,8 +2030,8 @@ class ChecklistTemplate(models.Model): def create_checklist_for_ticket(self, ticket): checklist = ticket.checklists.create(name=self.name) - for task in self.task_list: - checklist.tasks.create(description=task) + for position, task in enumerate(self.task_list): + checklist.tasks.create(description=task, position=position) return checklist @@ -2079,12 +2079,17 @@ class ChecklistTask(models.Model): 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/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..fbc9a8e7 --- /dev/null +++ b/helpdesk/templates/helpdesk/checklist_form.html @@ -0,0 +1,106 @@ +{% 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 %} + + {{ form.id }} + + + + + {% endfor %} + +
{% trans "Description" %}{% trans "Position" %}{% trans "Delete?" %}
+ {{ form.description }} + {{ form.description.errors }} + + {{ form.position }} + {{ form.position.errors }} + + {{ form.DELETE }} + {{ form.DELETE.errors }} +
+ +
+ + + {% trans "Cancel Changes" %} + + +
+
+
+
+{% endblock %} + +{% block helpdesk_js %} + +{% endblock %} diff --git a/helpdesk/templates/helpdesk/ticket.html b/helpdesk/templates/helpdesk/ticket.html index 265a449d..84185a9d 100644 --- a/helpdesk/templates/helpdesk/ticket.html +++ b/helpdesk/templates/helpdesk/ticket.html @@ -7,6 +7,11 @@ {% 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 %} diff --git a/helpdesk/templates/helpdesk/ticket_desc_table.html b/helpdesk/templates/helpdesk/ticket_desc_table.html index 36eba139..f7a2f710 100644 --- a/helpdesk/templates/helpdesk/ticket_desc_table.html +++ b/helpdesk/templates/helpdesk/ticket_desc_table.html @@ -178,14 +178,17 @@
{% for checklist in ticket.checklists.all %}
-
-
-

+
+
+
{{ checklist }} - -
+

@@ -214,9 +217,9 @@
{% endfor %}
-
+
-

Add a checklist

+
Add a checklist
diff --git a/helpdesk/urls.py b/helpdesk/urls.py index 1d001e18..762ac7af 100644 --- a/helpdesk/urls.py +++ b/helpdesk/urls.py @@ -93,6 +93,16 @@ urlpatterns = [ staff.attachment_del, name="attachment_del", ), + path( + "tickets//checklists//", + staff.edit_ticket_checklist, + name="edit_ticket_checklist" + ), + path( + "tickets//checklists//delete/", + staff.delete_ticket_checklist, + name="delete_ticket_checklist" + ), re_path(r"^raw/(?P\w+)/$", staff.raw_details, name="raw"), path("rss/", staff.rss_list, name="rss_index"), path("reports/", staff.report_index, name="report_index"), diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 40e8dbe1..e0576add 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -20,6 +20,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 inlineformset_factory, TextInput, NumberInput 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 @@ -49,7 +50,9 @@ from helpdesk.forms import ( TicketDependencyForm, TicketForm, UserSettingsForm, - ChecklistForm + CreateChecklistForm, + ChecklistForm, + FormControlDeleteFormSet ) from helpdesk.lib import process_attachments, queue_template_context, safe_template_context from helpdesk.models import ( @@ -65,7 +68,9 @@ from helpdesk.models import ( TicketChange, TicketCustomFieldValue, TicketDependency, - UserSettings + UserSettings, + Checklist, + ChecklistTask ) from helpdesk.query import get_query_class, query_from_base64, query_to_base64 from helpdesk.user import HelpdeskUser @@ -407,7 +412,7 @@ def view_ticket(request, ticket_id): else: submitter_userprofile_url = None - checklist_form = ChecklistForm(request.POST or None) + checklist_form = CreateChecklistForm(request.POST or None) if checklist_form.is_valid(): checklist_template = checklist_form.cleaned_data.get('checklist_template') if checklist_template: @@ -432,6 +437,55 @@ def view_ticket(request, ticket_id): }) +@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={ + 'description': TextInput(attrs={'class': 'form-control'}), + 'position': NumberInput(attrs={'class': 'form-control'}), + }, + can_delete=True, + extra=1 + ) + 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.POST: + checklist.delete() + return redirect(ticket) + + return render(request, 'helpdesk/checklist_confirm_delete.html', { + 'ticket': ticket, + 'checklist': checklist, + }) + + def return_ticketccstring_and_show_subscribe(user, ticket): """used in view_ticket() and followup_edit()""" # create the ticketcc_string and check whether current user is already From 308f69a03bfc48d41cee790db1dd5618b25110f1 Mon Sep 17 00:00:00 2001 From: Benbb96 Date: Sun, 30 Apr 2023 01:05:46 +0200 Subject: [PATCH 06/14] CRUD Checklist Template --- helpdesk/forms.py | 16 +++ helpdesk/models.py | 2 +- helpdesk/static/helpdesk/helpdesk-extend.css | 4 + .../checklist_template_confirm_delete.html | 47 +++++++ .../helpdesk/checklist_templates.html | 119 ++++++++++++++++++ .../templates/helpdesk/email_ignore_list.html | 7 ++ .../helpdesk/include/task_form_row.html | 13 ++ .../templates/helpdesk/system_settings.html | 1 + helpdesk/urls.py | 11 ++ helpdesk/views/staff.py | 33 ++++- 10 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 helpdesk/templates/helpdesk/checklist_template_confirm_delete.html create mode 100644 helpdesk/templates/helpdesk/checklist_templates.html create mode 100644 helpdesk/templates/helpdesk/include/task_form_row.html diff --git a/helpdesk/forms.py b/helpdesk/forms.py index b9a00ee3..f1496ccb 100644 --- a/helpdesk/forms.py +++ b/helpdesk/forms.py @@ -606,6 +606,22 @@ class MultipleTicketSelectForm(forms.Form): 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'}), diff --git a/helpdesk/models.py b/helpdesk/models.py index 7f9e2072..c2e90bb6 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -2010,7 +2010,7 @@ def is_a_list_without_empty_element(task_list): for task in task_list: if not isinstance(task, str): raise ValidationError(f'{task} is not a string') - if task == '': + if task.strip() == '': raise ValidationError('A task cannot be an empty string') 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_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..30db0975 --- /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/task_form_row.html' %} + {% endfor %} + {% else %} + {% include 'helpdesk/include/task_form_row.html' %} + {% endif %} + +
TaskActions
+ +
+ +
+
+
+
+
+ {% for checklist in checklists %} +
+ + {{ checklist.name }} + {% if checklist_template.id != checklist.id %} + + + + {% endif %} + + + + + {{ checklist.task_list|length }} 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..77a3af75 --- /dev/null +++ b/helpdesk/templates/helpdesk/include/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 @@