2008-08-19 10:50:38 +02:00
|
|
|
"""
|
2011-01-26 00:08:41 +01:00
|
|
|
django-helpdesk - A Django powered ticket tracker for small enterprise.
|
2007-12-27 01:29:17 +01:00
|
|
|
|
2008-02-06 05:36:07 +01:00
|
|
|
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
2007-12-27 01:29:17 +01:00
|
|
|
|
2008-08-19 10:50:38 +02:00
|
|
|
forms.py - Definitions of newforms-based forms for creating and maintaining
|
2008-02-06 05:36:07 +01:00
|
|
|
tickets.
|
2007-12-27 01:29:17 +01:00
|
|
|
"""
|
2008-08-19 10:50:38 +02:00
|
|
|
|
2022-07-22 03:26:41 +02:00
|
|
|
from datetime import datetime
|
2008-08-12 01:24:18 +02:00
|
|
|
from django import forms
|
2009-01-22 09:08:22 +01:00
|
|
|
from django.conf import settings
|
2016-10-21 17:14:12 +02:00
|
|
|
from django.contrib.auth import get_user_model
|
2022-07-22 03:26:41 +02:00
|
|
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
2016-11-21 03:12:19 +01:00
|
|
|
from django.utils import timezone
|
2022-07-22 03:26:41 +02:00
|
|
|
from django.utils.translation import gettext_lazy as _
|
2012-01-17 22:40:44 +01:00
|
|
|
from helpdesk import settings as helpdesk_settings
|
2022-07-22 03:26:41 +02:00
|
|
|
from helpdesk.lib import convert_value, process_attachments, safe_template_context
|
|
|
|
from helpdesk.models import (
|
2023-04-30 04:13:50 +02:00
|
|
|
Checklist,
|
|
|
|
ChecklistTemplate,
|
2022-07-22 03:26:41 +02:00
|
|
|
CustomField,
|
|
|
|
FollowUp,
|
|
|
|
IgnoreEmail,
|
|
|
|
Queue,
|
|
|
|
Ticket,
|
|
|
|
TicketCC,
|
|
|
|
TicketCustomFieldValue,
|
|
|
|
TicketDependency,
|
2023-04-30 04:13:50 +02:00
|
|
|
UserSettings
|
2022-07-22 03:26:41 +02:00
|
|
|
)
|
|
|
|
from helpdesk.settings import (
|
|
|
|
CUSTOMFIELD_DATE_FORMAT,
|
|
|
|
CUSTOMFIELD_DATETIME_FORMAT,
|
|
|
|
CUSTOMFIELD_TIME_FORMAT,
|
|
|
|
CUSTOMFIELD_TO_FIELD_DICT
|
|
|
|
)
|
2022-10-09 22:51:32 +02:00
|
|
|
from helpdesk.validators import validate_file_extension
|
2024-04-17 12:02:44 +02:00
|
|
|
from helpdesk.signals import new_ticket_done
|
2022-07-22 03:26:41 +02:00
|
|
|
import logging
|
|
|
|
|
2022-03-18 19:21:17 +01:00
|
|
|
|
|
|
|
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
2022-07-22 03:26:41 +02:00
|
|
|
from helpdesk.models import KBItem
|
2022-03-18 19:21:17 +01:00
|
|
|
|
2020-02-06 10:10:07 +01:00
|
|
|
logger = logging.getLogger(__name__)
|
2016-10-21 17:14:12 +02:00
|
|
|
User = get_user_model()
|
|
|
|
|
|
|
|
|
2014-06-05 01:45:07 +02:00
|
|
|
class CustomFieldMixin(object):
|
|
|
|
"""
|
|
|
|
Mixin that provides a method to turn CustomFields into an actual field
|
|
|
|
"""
|
2016-10-23 22:09:17 +02:00
|
|
|
|
2014-06-05 01:45:07 +02:00
|
|
|
def customfield_to_field(self, field, instanceargs):
|
2020-06-03 10:21:43 +02:00
|
|
|
# Use TextInput widget by default
|
2022-07-12 12:34:19 +02:00
|
|
|
instanceargs['widget'] = forms.TextInput(
|
|
|
|
attrs={'class': 'form-control'})
|
2016-10-30 20:53:18 +01:00
|
|
|
# if-elif branches start with special cases
|
2014-06-05 01:45:07 +02:00
|
|
|
if field.data_type == 'varchar':
|
|
|
|
fieldclass = forms.CharField
|
|
|
|
instanceargs['max_length'] = field.max_length
|
|
|
|
elif field.data_type == 'text':
|
|
|
|
fieldclass = forms.CharField
|
2022-07-12 12:34:19 +02:00
|
|
|
instanceargs['widget'] = forms.Textarea(
|
|
|
|
attrs={'class': 'form-control'})
|
2014-06-05 01:45:07 +02:00
|
|
|
instanceargs['max_length'] = field.max_length
|
|
|
|
elif field.data_type == 'integer':
|
|
|
|
fieldclass = forms.IntegerField
|
2022-07-12 12:34:19 +02:00
|
|
|
instanceargs['widget'] = forms.NumberInput(
|
|
|
|
attrs={'class': 'form-control'})
|
2014-06-05 01:45:07 +02:00
|
|
|
elif field.data_type == 'decimal':
|
|
|
|
fieldclass = forms.DecimalField
|
|
|
|
instanceargs['decimal_places'] = field.decimal_places
|
|
|
|
instanceargs['max_digits'] = field.max_length
|
2022-07-12 12:34:19 +02:00
|
|
|
instanceargs['widget'] = forms.NumberInput(
|
|
|
|
attrs={'class': 'form-control'})
|
2014-06-05 01:45:07 +02:00
|
|
|
elif field.data_type == 'list':
|
|
|
|
fieldclass = forms.ChoiceField
|
2022-03-31 17:19:49 +02:00
|
|
|
instanceargs['choices'] = field.get_choices()
|
2022-07-12 12:34:19 +02:00
|
|
|
instanceargs['widget'] = forms.Select(
|
|
|
|
attrs={'class': 'form-control'})
|
2016-10-30 08:38:49 +01:00
|
|
|
else:
|
2016-10-30 20:53:18 +01:00
|
|
|
# Try to use the immediate equivalences dictionary
|
|
|
|
try:
|
|
|
|
fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type]
|
2021-02-11 01:58:01 +01:00
|
|
|
# Change widgets for the following classes
|
|
|
|
if fieldclass == forms.DateField:
|
2022-07-12 12:34:19 +02:00
|
|
|
instanceargs['widget'] = forms.DateInput(
|
|
|
|
attrs={'class': 'form-control date-field'})
|
2021-02-11 01:58:01 +01:00
|
|
|
elif fieldclass == forms.DateTimeField:
|
2022-07-12 12:34:19 +02:00
|
|
|
instanceargs['widget'] = forms.DateTimeInput(
|
|
|
|
attrs={'class': 'form-control datetime-field'})
|
2021-02-11 01:58:01 +01:00
|
|
|
elif fieldclass == forms.TimeField:
|
2022-07-12 12:34:19 +02:00
|
|
|
instanceargs['widget'] = forms.TimeInput(
|
|
|
|
attrs={'class': 'form-control time-field'})
|
2021-02-11 01:58:01 +01:00
|
|
|
elif fieldclass == forms.BooleanField:
|
2022-07-12 12:34:19 +02:00
|
|
|
instanceargs['widget'] = forms.CheckboxInput(
|
|
|
|
attrs={'class': 'form-control'})
|
2020-06-03 10:21:43 +02:00
|
|
|
|
2016-10-30 20:53:18 +01:00
|
|
|
except KeyError:
|
|
|
|
# The data_type was not found anywhere
|
|
|
|
raise NameError("Unrecognized data_type %s" % field.data_type)
|
2014-06-05 01:45:07 +02:00
|
|
|
|
|
|
|
self.fields['custom_%s' % field.name] = fieldclass(**instanceargs)
|
|
|
|
|
2016-10-21 17:14:12 +02:00
|
|
|
|
2014-06-05 01:45:07 +02:00
|
|
|
class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
2016-10-23 22:09:17 +02:00
|
|
|
|
2009-06-03 13:43:46 +02:00
|
|
|
class Meta:
|
|
|
|
model = Ticket
|
2022-07-12 12:34:19 +02:00
|
|
|
exclude = ('created', 'modified', 'status', 'on_hold',
|
|
|
|
'resolution', 'last_escalation', 'assigned_to')
|
2016-09-12 08:11:55 +02:00
|
|
|
|
2021-02-11 01:58:01 +01:00
|
|
|
class Media:
|
2022-07-12 12:34:19 +02:00
|
|
|
js = ('helpdesk/js/init_due_date.js',
|
|
|
|
'helpdesk/js/init_datetime_classes.js')
|
2021-02-11 01:58:01 +01:00
|
|
|
|
2011-05-09 23:54:44 +02:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Add any custom fields that are defined to the form
|
|
|
|
"""
|
|
|
|
super(EditTicketForm, self).__init__(*args, **kwargs)
|
|
|
|
|
2021-02-09 21:08:27 +01:00
|
|
|
# Disable and add help_text to the merged_to field on this form
|
|
|
|
self.fields['merged_to'].disabled = True
|
2022-07-12 12:34:19 +02:00
|
|
|
self.fields['merged_to'].help_text = _(
|
|
|
|
'This ticket is merged into the selected ticket.')
|
2021-02-09 21:08:27 +01:00
|
|
|
|
2011-05-09 23:54:44 +02:00
|
|
|
for field in CustomField.objects.all():
|
2021-02-11 01:58:01 +01:00
|
|
|
initial_value = None
|
2011-05-09 23:54:44 +02:00
|
|
|
try:
|
2022-07-12 12:34:19 +02:00
|
|
|
current_value = TicketCustomFieldValue.objects.get(
|
|
|
|
ticket=self.instance, field=field)
|
2011-05-09 23:54:44 +02:00
|
|
|
initial_value = current_value.value
|
2022-07-12 12:34:19 +02:00
|
|
|
# Attempt to convert from fixed format string to date/time data
|
|
|
|
# type
|
2021-02-11 01:58:01 +01:00
|
|
|
if 'datetime' == current_value.field.data_type:
|
2022-07-12 12:34:19 +02:00
|
|
|
initial_value = datetime.strptime(
|
|
|
|
initial_value, CUSTOMFIELD_DATETIME_FORMAT)
|
2021-02-11 01:58:01 +01:00
|
|
|
elif 'date' == current_value.field.data_type:
|
2022-07-12 12:34:19 +02:00
|
|
|
initial_value = datetime.strptime(
|
|
|
|
initial_value, CUSTOMFIELD_DATE_FORMAT)
|
2021-02-11 01:58:01 +01:00
|
|
|
elif 'time' == current_value.field.data_type:
|
2022-07-12 12:34:19 +02:00
|
|
|
initial_value = datetime.strptime(
|
|
|
|
initial_value, CUSTOMFIELD_TIME_FORMAT)
|
|
|
|
# If it is boolean field, transform the value to a real boolean
|
|
|
|
# instead of a string
|
2021-02-11 01:58:01 +01:00
|
|
|
elif 'boolean' == current_value.field.data_type:
|
|
|
|
initial_value = 'True' == initial_value
|
|
|
|
except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError):
|
|
|
|
# ValueError error if parsing fails, using initial_value = current_value.value
|
|
|
|
# TypeError if parsing None type
|
|
|
|
pass
|
2011-05-09 23:54:44 +02:00
|
|
|
instanceargs = {
|
2016-10-23 22:09:17 +02:00
|
|
|
'label': field.label,
|
|
|
|
'help_text': field.help_text,
|
|
|
|
'required': field.required,
|
|
|
|
'initial': initial_value,
|
|
|
|
}
|
2014-06-05 01:45:07 +02:00
|
|
|
|
|
|
|
self.customfield_to_field(field, instanceargs)
|
2011-05-09 23:54:44 +02:00
|
|
|
|
|
|
|
def save(self, *args, **kwargs):
|
2016-09-12 08:11:55 +02:00
|
|
|
|
2011-05-09 23:54:44 +02:00
|
|
|
for field, value in self.cleaned_data.items():
|
|
|
|
if field.startswith('custom_'):
|
2014-10-25 21:53:56 +02:00
|
|
|
field_name = field.replace('custom_', '', 1)
|
2011-05-09 23:54:44 +02:00
|
|
|
customfield = CustomField.objects.get(name=field_name)
|
|
|
|
try:
|
2022-07-12 12:34:19 +02:00
|
|
|
cfv = TicketCustomFieldValue.objects.get(
|
|
|
|
ticket=self.instance, field=customfield)
|
2016-10-21 17:14:12 +02:00
|
|
|
except ObjectDoesNotExist:
|
2022-07-12 12:34:19 +02:00
|
|
|
cfv = TicketCustomFieldValue(
|
|
|
|
ticket=self.instance, field=customfield)
|
2021-02-11 01:58:01 +01:00
|
|
|
|
2022-03-31 17:19:49 +02:00
|
|
|
cfv.value = convert_value(value)
|
2011-05-09 23:54:44 +02:00
|
|
|
cfv.save()
|
2016-09-12 08:11:55 +02:00
|
|
|
|
2011-05-09 23:54:44 +02:00
|
|
|
return super(EditTicketForm, self).save(*args, **kwargs)
|
|
|
|
|
2009-06-03 13:43:46 +02:00
|
|
|
|
2024-07-26 10:46:26 +02:00
|
|
|
class EditTicketCustomFieldForm(EditTicketForm):
|
|
|
|
"""
|
|
|
|
Uses the EditTicketForm logic to provide a form for Ticket custom fields.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Add any custom fields that are defined to the form
|
|
|
|
"""
|
|
|
|
super(EditTicketCustomFieldForm, self).__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
del self.fields['merged_to']
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = Ticket
|
|
|
|
exclude = ('title', 'queue', 'created', 'modified',
|
|
|
|
'submitter_email', 'assigned_to', 'status',
|
|
|
|
'on_hold', 'description', 'resolution', 'priority',
|
|
|
|
'due_date', 'last_escalation', 'secret_key', 'kbitem')
|
|
|
|
|
|
|
|
|
2011-01-29 07:02:03 +01:00
|
|
|
class EditFollowUpForm(forms.ModelForm):
|
2016-10-23 22:09:17 +02:00
|
|
|
|
2011-01-29 07:02:03 +01:00
|
|
|
class Meta:
|
|
|
|
model = FollowUp
|
|
|
|
exclude = ('date', 'user',)
|
|
|
|
|
2016-10-21 17:14:12 +02:00
|
|
|
def __init__(self, *args, **kwargs):
|
2024-02-01 15:38:02 +01:00
|
|
|
"""Filter not opened tickets here."""
|
2016-10-21 17:14:12 +02:00
|
|
|
super(EditFollowUpForm, self).__init__(*args, **kwargs)
|
2022-07-12 12:34:19 +02:00
|
|
|
self.fields["ticket"].queryset = Ticket.objects.filter(
|
2024-02-01 15:38:02 +01:00
|
|
|
status__in=Ticket.OPEN_STATUSES)
|
2016-10-21 17:14:12 +02:00
|
|
|
|
|
|
|
|
2016-10-30 08:39:06 +01:00
|
|
|
class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
|
|
|
"""
|
|
|
|
Contain all the common code and fields between "TicketForm" and
|
|
|
|
"PublicTicketForm". This Form is not intended to be used directly.
|
|
|
|
"""
|
2008-08-19 10:50:38 +02:00
|
|
|
queue = forms.ChoiceField(
|
2016-10-29 10:08:57 +02:00
|
|
|
widget=forms.Select(attrs={'class': 'form-control'}),
|
2008-08-19 10:50:38 +02:00
|
|
|
label=_('Queue'),
|
|
|
|
required=True,
|
|
|
|
choices=()
|
2016-10-23 22:09:17 +02:00
|
|
|
)
|
2008-08-19 10:50:38 +02:00
|
|
|
|
|
|
|
title = forms.CharField(
|
|
|
|
max_length=100,
|
|
|
|
required=True,
|
2016-10-29 10:08:57 +02:00
|
|
|
widget=forms.TextInput(attrs={'class': 'form-control'}),
|
2008-08-19 10:50:38 +02:00
|
|
|
label=_('Summary of the problem'),
|
2016-10-23 22:09:17 +02:00
|
|
|
)
|
2008-08-19 10:50:38 +02:00
|
|
|
|
|
|
|
body = forms.CharField(
|
2016-10-31 06:38:49 +01:00
|
|
|
widget=forms.Textarea(attrs={'class': 'form-control'}),
|
2016-10-30 08:39:06 +01:00
|
|
|
label=_('Description of your issue'),
|
2008-08-19 10:50:38 +02:00
|
|
|
required=True,
|
2022-07-12 12:34:19 +02:00
|
|
|
help_text=_(
|
|
|
|
'Please be as descriptive as possible and include all details'),
|
2016-10-23 22:09:17 +02:00
|
|
|
)
|
2008-08-19 10:50:38 +02:00
|
|
|
|
|
|
|
priority = forms.ChoiceField(
|
2016-10-29 10:08:57 +02:00
|
|
|
widget=forms.Select(attrs={'class': 'form-control'}),
|
2008-08-19 10:50:38 +02:00
|
|
|
choices=Ticket.PRIORITY_CHOICES,
|
2016-10-30 08:39:06 +01:00
|
|
|
required=True,
|
2020-02-06 10:10:07 +01:00
|
|
|
initial=getattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY', '3'),
|
2008-08-19 10:50:38 +02:00
|
|
|
label=_('Priority'),
|
2022-07-12 12:34:19 +02:00
|
|
|
help_text=_(
|
|
|
|
"Please select a priority carefully. If unsure, leave it as '3'."),
|
2016-10-23 22:09:17 +02:00
|
|
|
)
|
2008-08-19 10:50:38 +02:00
|
|
|
|
2012-01-20 21:48:38 +01:00
|
|
|
due_date = forms.DateTimeField(
|
2022-07-12 12:34:19 +02:00
|
|
|
widget=forms.TextInput(
|
|
|
|
attrs={'class': 'form-control', 'autocomplete': 'off'}),
|
2012-01-20 21:48:38 +01:00
|
|
|
required=False,
|
2022-07-12 12:34:19 +02:00
|
|
|
input_formats=[CUSTOMFIELD_DATE_FORMAT,
|
|
|
|
CUSTOMFIELD_DATETIME_FORMAT, '%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"],
|
2012-01-20 21:48:38 +01:00
|
|
|
label=_('Due on'),
|
2016-10-23 22:09:17 +02:00
|
|
|
)
|
2012-01-20 21:48:38 +01:00
|
|
|
|
2024-06-06 15:47:50 +02:00
|
|
|
if helpdesk_settings.HELPDESK_ENABLE_ATTACHMENTS:
|
|
|
|
attachment = forms.FileField(
|
|
|
|
widget=forms.FileInput(attrs={'class': 'form-control-file'}),
|
|
|
|
required=False,
|
|
|
|
label=_('Attach File'),
|
|
|
|
help_text=_('You can attach a file to this ticket. '
|
|
|
|
'Only file types such as plain text (.txt), '
|
|
|
|
'a document (.pdf, .docx, or .odt), '
|
|
|
|
'or screenshot (.png or .jpg) may be uploaded.'),
|
|
|
|
validators=[validate_file_extension]
|
|
|
|
)
|
|
|
|
|
2020-10-23 16:23:47 +02:00
|
|
|
class Media:
|
2022-07-12 12:34:19 +02:00
|
|
|
js = ('helpdesk/js/init_due_date.js',
|
|
|
|
'helpdesk/js/init_datetime_classes.js')
|
2020-10-23 16:23:47 +02:00
|
|
|
|
2020-01-08 18:39:41 +01:00
|
|
|
def __init__(self, kbcategory=None, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
2022-03-18 19:21:17 +01:00
|
|
|
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
|
|
|
if kbcategory:
|
|
|
|
self.fields['kbitem'] = forms.ChoiceField(
|
|
|
|
widget=forms.Select(attrs={'class': 'form-control'}),
|
|
|
|
required=False,
|
|
|
|
label=_('Knowledge Base Item'),
|
2022-07-12 12:34:19 +02:00
|
|
|
choices=[(kbi.pk, kbi.title) for kbi in KBItem.objects.filter(
|
|
|
|
category=kbcategory.pk, enabled=True)],
|
2022-03-18 19:21:17 +01:00
|
|
|
)
|
2020-01-08 18:39:41 +01:00
|
|
|
|
2016-10-30 08:39:06 +01:00
|
|
|
def _add_form_custom_fields(self, staff_only_filter=None):
|
|
|
|
if staff_only_filter is None:
|
|
|
|
queryset = CustomField.objects.all()
|
|
|
|
else:
|
|
|
|
queryset = CustomField.objects.filter(staff_only=staff_only_filter)
|
|
|
|
|
|
|
|
for field in queryset:
|
2020-01-08 20:38:08 +01:00
|
|
|
instanceargs = {
|
|
|
|
'label': field.label,
|
|
|
|
'help_text': field.help_text,
|
|
|
|
'required': field.required,
|
|
|
|
}
|
2014-06-05 01:45:07 +02:00
|
|
|
|
|
|
|
self.customfield_to_field(field, instanceargs)
|
2011-02-02 12:22:46 +01:00
|
|
|
|
2020-01-07 13:47:36 +01:00
|
|
|
def _get_queue(self):
|
2020-02-06 10:10:07 +01:00
|
|
|
# this procedure is re-defined for public submission form
|
2020-01-07 13:47:36 +01:00
|
|
|
return Queue.objects.get(id=int(self.cleaned_data['queue']))
|
|
|
|
|
2016-10-30 08:39:06 +01:00
|
|
|
def _create_ticket(self):
|
2020-02-06 10:10:07 +01:00
|
|
|
queue = self._get_queue()
|
2020-01-07 13:33:06 +01:00
|
|
|
kbitem = None
|
|
|
|
if 'kbitem' in self.cleaned_data:
|
|
|
|
kbitem = KBItem.objects.get(id=int(self.cleaned_data['kbitem']))
|
2008-08-19 10:50:38 +02:00
|
|
|
|
2020-02-06 10:10:07 +01:00
|
|
|
ticket = Ticket(
|
|
|
|
title=self.cleaned_data['title'],
|
|
|
|
submitter_email=self.cleaned_data['submitter_email'],
|
|
|
|
created=timezone.now(),
|
|
|
|
status=Ticket.OPEN_STATUS,
|
|
|
|
queue=queue,
|
|
|
|
description=self.cleaned_data['body'],
|
|
|
|
priority=self.cleaned_data.get(
|
|
|
|
'priority',
|
|
|
|
getattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY", "3")
|
|
|
|
),
|
|
|
|
due_date=self.cleaned_data.get(
|
|
|
|
'due_date',
|
|
|
|
getattr(settings, "HELPDESK_PUBLIC_TICKET_DUE_DATE", None)
|
|
|
|
) or None,
|
|
|
|
kbitem=kbitem,
|
|
|
|
)
|
2008-08-19 10:50:38 +02:00
|
|
|
|
2016-10-30 20:43:05 +01:00
|
|
|
return ticket, queue
|
2016-09-12 08:11:55 +02:00
|
|
|
|
2016-10-30 08:39:06 +01:00
|
|
|
def _create_custom_fields(self, ticket):
|
2022-03-31 17:19:49 +02:00
|
|
|
ticket.save_custom_field_values(self.cleaned_data)
|
2007-12-27 01:29:17 +01:00
|
|
|
|
2016-10-30 08:39:06 +01:00
|
|
|
def _create_follow_up(self, ticket, title, user=None):
|
2016-10-30 20:43:05 +01:00
|
|
|
followup = FollowUp(ticket=ticket,
|
|
|
|
title=title,
|
|
|
|
date=timezone.now(),
|
|
|
|
public=True,
|
|
|
|
comment=self.cleaned_data['body'],
|
|
|
|
)
|
2016-10-30 08:39:06 +01:00
|
|
|
if user:
|
2016-10-30 20:43:05 +01:00
|
|
|
followup.user = user
|
|
|
|
return followup
|
2007-12-28 04:29:45 +01:00
|
|
|
|
2016-10-30 08:39:06 +01:00
|
|
|
def _attach_files_to_follow_up(self, followup):
|
2024-06-12 14:58:00 +02:00
|
|
|
files = self.cleaned_data.get('attachment')
|
2016-11-10 17:23:16 +01:00
|
|
|
if files:
|
|
|
|
files = process_attachments(followup, [files])
|
|
|
|
return files
|
2008-08-19 10:50:38 +02:00
|
|
|
|
2016-10-30 08:39:06 +01:00
|
|
|
@staticmethod
|
|
|
|
def _send_messages(ticket, queue, followup, files, user=None):
|
|
|
|
context = safe_template_context(ticket)
|
|
|
|
context['comment'] = followup.comment
|
2016-09-12 08:11:55 +02:00
|
|
|
|
2018-10-31 16:24:57 +01:00
|
|
|
roles = {'submitter': ('newticket_submitter', context),
|
|
|
|
'new_ticket_cc': ('newticket_cc', context),
|
|
|
|
'ticket_cc': ('newticket_cc', context)}
|
|
|
|
if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign:
|
|
|
|
roles['assigned_to'] = ('assigned_owner', context)
|
|
|
|
ticket.send(
|
|
|
|
roles,
|
|
|
|
fail_silently=True,
|
|
|
|
files=files,
|
|
|
|
)
|
2008-01-21 02:02:12 +01:00
|
|
|
|
2008-08-19 10:50:38 +02:00
|
|
|
|
2016-10-30 08:39:06 +01:00
|
|
|
class TicketForm(AbstractTicketForm):
|
|
|
|
"""
|
|
|
|
Ticket Form creation for registered users.
|
|
|
|
"""
|
2008-08-19 10:50:38 +02:00
|
|
|
submitter_email = forms.EmailField(
|
2012-01-20 21:48:38 +01:00
|
|
|
required=False,
|
2016-10-30 08:39:06 +01:00
|
|
|
label=_('Submitter E-Mail Address'),
|
2022-07-12 12:34:19 +02:00
|
|
|
widget=forms.TextInput(
|
|
|
|
attrs={'class': 'form-control', 'type': 'email'}),
|
2016-10-30 08:39:06 +01:00
|
|
|
help_text=_('This e-mail address will receive copies of all public '
|
|
|
|
'updates to this ticket.'),
|
2016-10-23 22:09:17 +02:00
|
|
|
)
|
2016-10-30 08:39:06 +01:00
|
|
|
assigned_to = forms.ChoiceField(
|
2020-02-06 10:10:07 +01:00
|
|
|
widget=(
|
|
|
|
forms.Select(attrs={'class': 'form-control'})
|
|
|
|
if not helpdesk_settings.HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO
|
|
|
|
else forms.HiddenInput()
|
|
|
|
),
|
2009-01-22 09:08:22 +01:00
|
|
|
required=False,
|
2016-10-30 08:39:06 +01:00
|
|
|
label=_('Case owner'),
|
|
|
|
help_text=_('If you select an owner other than yourself, they\'ll be '
|
|
|
|
'e-mailed details of this ticket immediately.'),
|
2018-09-07 19:05:16 +02:00
|
|
|
|
|
|
|
choices=()
|
2016-10-23 22:09:17 +02:00
|
|
|
)
|
2009-01-22 09:08:22 +01:00
|
|
|
|
2011-02-02 12:22:46 +01:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
"""
|
2016-10-30 08:39:06 +01:00
|
|
|
Add any custom fields that are defined to the form.
|
2011-02-02 12:22:46 +01:00
|
|
|
"""
|
2019-02-05 14:38:41 +01:00
|
|
|
queue_choices = kwargs.pop("queue_choices")
|
|
|
|
|
2018-09-07 19:05:16 +02:00
|
|
|
super().__init__(*args, **kwargs)
|
2019-02-05 14:38:41 +01:00
|
|
|
|
|
|
|
self.fields['queue'].choices = queue_choices
|
2018-09-07 19:05:16 +02:00
|
|
|
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS:
|
2022-07-12 12:34:19 +02:00
|
|
|
assignable_users = User.objects.filter(
|
|
|
|
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
|
2018-09-07 19:05:16 +02:00
|
|
|
else:
|
2022-07-12 12:34:19 +02:00
|
|
|
assignable_users = User.objects.filter(
|
|
|
|
is_active=True).order_by(User.USERNAME_FIELD)
|
|
|
|
self.fields['assigned_to'].choices = [
|
|
|
|
('', '--------')] + [(u.id, u.get_username()) for u in assignable_users]
|
2016-10-30 08:39:06 +01:00
|
|
|
self._add_form_custom_fields()
|
2011-02-02 12:22:46 +01:00
|
|
|
|
2020-03-03 22:39:02 +01:00
|
|
|
def save(self, user):
|
2008-01-16 01:26:24 +01:00
|
|
|
"""
|
|
|
|
Writes and returns a Ticket() object
|
|
|
|
"""
|
2008-08-19 10:50:38 +02:00
|
|
|
|
2016-10-30 20:43:05 +01:00
|
|
|
ticket, queue = self._create_ticket()
|
2016-10-30 08:39:06 +01:00
|
|
|
if self.cleaned_data['assigned_to']:
|
|
|
|
try:
|
|
|
|
u = User.objects.get(id=self.cleaned_data['assigned_to'])
|
2016-10-30 20:43:05 +01:00
|
|
|
ticket.assigned_to = u
|
2016-10-30 08:39:06 +01:00
|
|
|
except User.DoesNotExist:
|
2016-10-30 20:43:05 +01:00
|
|
|
ticket.assigned_to = None
|
|
|
|
ticket.save()
|
2016-09-12 08:11:55 +02:00
|
|
|
|
2016-10-30 20:43:05 +01:00
|
|
|
self._create_custom_fields(ticket)
|
2009-01-22 09:08:22 +01:00
|
|
|
|
2016-10-30 08:39:06 +01:00
|
|
|
if self.cleaned_data['assigned_to']:
|
|
|
|
title = _('Ticket Opened & Assigned to %(name)s') % {
|
2016-10-30 20:43:05 +01:00
|
|
|
'name': ticket.get_assigned_to or _("<invalid user>")
|
2016-10-30 08:39:06 +01:00
|
|
|
}
|
|
|
|
else:
|
|
|
|
title = _('Ticket Opened')
|
2016-10-30 20:43:05 +01:00
|
|
|
followup = self._create_follow_up(ticket, title=title, user=user)
|
|
|
|
followup.save()
|
2008-01-16 01:26:24 +01:00
|
|
|
|
2024-06-12 15:04:23 +02:00
|
|
|
if helpdesk_settings.HELPDESK_ENABLE_ATTACHMENTS:
|
2024-06-12 14:58:00 +02:00
|
|
|
files = self._attach_files_to_follow_up(followup)
|
|
|
|
else:
|
|
|
|
files = None
|
2024-04-17 12:02:44 +02:00
|
|
|
|
|
|
|
# emit signal when the TicketForm.save is done
|
|
|
|
new_ticket_done.send(sender="TicketForm", ticket=ticket)
|
|
|
|
|
2016-10-30 20:43:05 +01:00
|
|
|
self._send_messages(ticket=ticket,
|
|
|
|
queue=queue,
|
|
|
|
followup=followup,
|
|
|
|
files=files,
|
|
|
|
user=user)
|
|
|
|
return ticket
|
2009-08-11 11:02:48 +02:00
|
|
|
|
2016-02-17 09:40:08 +01:00
|
|
|
|
2016-10-30 08:39:06 +01:00
|
|
|
class PublicTicketForm(AbstractTicketForm):
|
|
|
|
"""
|
|
|
|
Ticket Form creation for all users (public-facing).
|
|
|
|
"""
|
|
|
|
submitter_email = forms.EmailField(
|
2022-07-12 12:34:19 +02:00
|
|
|
widget=forms.TextInput(
|
|
|
|
attrs={'class': 'form-control', 'type': 'email'}),
|
2016-10-30 08:39:06 +01:00
|
|
|
required=True,
|
|
|
|
label=_('Your E-Mail Address'),
|
|
|
|
help_text=_('We will e-mail you when your ticket is updated.'),
|
|
|
|
)
|
2008-08-19 10:50:38 +02:00
|
|
|
|
2020-01-08 18:39:41 +01:00
|
|
|
def __init__(self, hidden_fields=(), readonly_fields=(), *args, **kwargs):
|
2016-10-30 08:39:06 +01:00
|
|
|
"""
|
|
|
|
Add any (non-staff) custom fields that are defined to the form
|
|
|
|
"""
|
|
|
|
super(PublicTicketForm, self).__init__(*args, **kwargs)
|
2019-12-12 16:41:55 +01:00
|
|
|
self._add_form_custom_fields(False)
|
|
|
|
|
2021-08-19 22:00:18 +02:00
|
|
|
for field in self.fields.keys():
|
|
|
|
if field in hidden_fields:
|
|
|
|
self.fields[field].widget = forms.HiddenInput()
|
|
|
|
if field in readonly_fields:
|
|
|
|
self.fields[field].disabled = True
|
|
|
|
|
|
|
|
field_deletion_table = {
|
2019-12-06 15:44:20 +01:00
|
|
|
'queue': 'HELPDESK_PUBLIC_TICKET_QUEUE',
|
|
|
|
'priority': 'HELPDESK_PUBLIC_TICKET_PRIORITY',
|
|
|
|
'due_date': 'HELPDESK_PUBLIC_TICKET_DUE_DATE',
|
|
|
|
}
|
2019-12-12 17:22:57 +01:00
|
|
|
|
2021-08-19 22:00:18 +02:00
|
|
|
for field_name, field_setting_key in field_deletion_table.items():
|
2022-07-12 12:34:19 +02:00
|
|
|
has_settings_default_value = getattr(
|
|
|
|
settings, field_setting_key, None)
|
2020-02-06 10:10:07 +01:00
|
|
|
if has_settings_default_value is not None:
|
2021-08-19 22:00:18 +02:00
|
|
|
del self.fields[field_name]
|
2019-12-06 15:44:20 +01:00
|
|
|
|
2020-02-06 10:10:07 +01:00
|
|
|
public_queues = Queue.objects.filter(allow_public_submission=True)
|
|
|
|
|
|
|
|
if len(public_queues) == 0:
|
|
|
|
logger.warning(
|
|
|
|
"There are no public queues defined - public ticket creation is impossible"
|
|
|
|
)
|
|
|
|
|
|
|
|
if 'queue' in self.fields:
|
|
|
|
self.fields['queue'].choices = [('', '--------')] + [
|
|
|
|
(q.id, q.title) for q in public_queues]
|
|
|
|
|
|
|
|
def _get_queue(self):
|
|
|
|
if getattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE', None) is not None:
|
|
|
|
# force queue to be the pre-defined one
|
|
|
|
# (only for public submissions)
|
|
|
|
public_queue = Queue.objects.filter(
|
|
|
|
slug=settings.HELPDESK_PUBLIC_TICKET_QUEUE
|
|
|
|
).first()
|
|
|
|
if not public_queue:
|
|
|
|
logger.fatal(
|
|
|
|
"Public queue '%s' is configured as default but can't be found",
|
|
|
|
settings.HELPDESK_PUBLIC_TICKET_QUEUE
|
|
|
|
)
|
|
|
|
return public_queue
|
|
|
|
else:
|
|
|
|
# get the queue user entered
|
|
|
|
return Queue.objects.get(id=int(self.cleaned_data['queue']))
|
2008-01-21 02:02:12 +01:00
|
|
|
|
2020-03-03 22:39:02 +01:00
|
|
|
def save(self, user):
|
2016-10-30 08:39:06 +01:00
|
|
|
"""
|
|
|
|
Writes and returns a Ticket() object
|
|
|
|
"""
|
2016-10-30 20:43:05 +01:00
|
|
|
ticket, queue = self._create_ticket()
|
|
|
|
if queue.default_owner and not ticket.assigned_to:
|
|
|
|
ticket.assigned_to = queue.default_owner
|
|
|
|
ticket.save()
|
|
|
|
|
|
|
|
self._create_custom_fields(ticket)
|
|
|
|
|
2020-03-03 22:39:02 +01:00
|
|
|
followup = self._create_follow_up(
|
|
|
|
ticket, title=_('Ticket Opened Via Web'), user=user)
|
2016-10-30 20:43:05 +01:00
|
|
|
followup.save()
|
|
|
|
|
|
|
|
files = self._attach_files_to_follow_up(followup)
|
2024-04-17 12:02:44 +02:00
|
|
|
|
|
|
|
# emit signal when the PublicTicketForm.save is done
|
|
|
|
new_ticket_done.send(sender="PublicTicketForm", ticket=ticket)
|
|
|
|
|
2016-10-30 20:43:05 +01:00
|
|
|
self._send_messages(ticket=ticket,
|
|
|
|
queue=queue,
|
|
|
|
followup=followup,
|
|
|
|
files=files)
|
|
|
|
return ticket
|
2008-08-19 10:50:38 +02:00
|
|
|
|
2008-09-09 10:32:01 +02:00
|
|
|
|
2018-10-05 14:54:22 +02:00
|
|
|
class UserSettingsForm(forms.ModelForm):
|
2009-07-22 10:19:46 +02:00
|
|
|
|
2018-10-05 14:54:22 +02:00
|
|
|
class Meta:
|
|
|
|
model = UserSettings
|
|
|
|
exclude = ['user', 'settings_pickled']
|
2009-08-06 10:56:02 +02:00
|
|
|
|
|
|
|
|
2008-10-25 00:52:34 +02:00
|
|
|
class EmailIgnoreForm(forms.ModelForm):
|
2016-10-23 22:09:17 +02:00
|
|
|
|
2008-10-25 00:52:34 +02:00
|
|
|
class Meta:
|
|
|
|
model = IgnoreEmail
|
2014-09-11 09:37:51 +02:00
|
|
|
exclude = []
|
2009-09-09 10:47:48 +02:00
|
|
|
|
2016-10-21 17:14:12 +02:00
|
|
|
|
2009-09-09 10:47:48 +02:00
|
|
|
class TicketCCForm(forms.ModelForm):
|
2016-09-12 08:11:55 +02:00
|
|
|
''' Adds either an email address or helpdesk user as a CC on a Ticket. Used for processing POST requests. '''
|
2016-10-23 22:09:17 +02:00
|
|
|
|
2016-10-21 17:14:12 +02:00
|
|
|
class Meta:
|
|
|
|
model = TicketCC
|
|
|
|
exclude = ('ticket',)
|
|
|
|
|
2011-02-06 18:49:07 +01:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super(TicketCCForm, self).__init__(*args, **kwargs)
|
2012-01-17 22:40:44 +01:00
|
|
|
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
|
2022-07-12 12:34:19 +02:00
|
|
|
users = User.objects.filter(
|
|
|
|
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
|
2012-01-17 22:40:44 +01:00
|
|
|
else:
|
2022-07-12 12:34:19 +02:00
|
|
|
users = User.objects.filter(
|
|
|
|
is_active=True).order_by(User.USERNAME_FIELD)
|
2016-09-12 08:11:55 +02:00
|
|
|
self.fields['user'].queryset = users
|
2011-02-06 18:49:07 +01:00
|
|
|
|
2016-10-29 10:27:29 +02:00
|
|
|
|
2016-09-12 08:11:55 +02:00
|
|
|
class TicketCCUserForm(forms.ModelForm):
|
|
|
|
''' Adds a helpdesk user as a CC on a Ticket '''
|
2016-10-29 10:08:57 +02:00
|
|
|
|
2016-09-12 08:11:55 +02:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super(TicketCCUserForm, self).__init__(*args, **kwargs)
|
|
|
|
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
|
2022-07-12 12:34:19 +02:00
|
|
|
users = User.objects.filter(
|
|
|
|
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
|
2016-09-12 08:11:55 +02:00
|
|
|
else:
|
2022-07-12 12:34:19 +02:00
|
|
|
users = User.objects.filter(
|
|
|
|
is_active=True).order_by(User.USERNAME_FIELD)
|
2016-09-12 08:11:55 +02:00
|
|
|
self.fields['user'].queryset = users
|
2016-10-29 10:20:16 +02:00
|
|
|
|
2016-09-12 08:11:55 +02:00
|
|
|
class Meta:
|
|
|
|
model = TicketCC
|
2016-10-29 10:08:57 +02:00
|
|
|
exclude = ('ticket', 'email',)
|
2016-09-12 08:11:55 +02:00
|
|
|
|
2016-10-29 10:27:29 +02:00
|
|
|
|
2016-09-12 08:11:55 +02:00
|
|
|
class TicketCCEmailForm(forms.ModelForm):
|
|
|
|
''' Adds an email address as a CC on a Ticket '''
|
2016-10-29 10:08:57 +02:00
|
|
|
|
2016-09-12 08:11:55 +02:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super(TicketCCEmailForm, self).__init__(*args, **kwargs)
|
2016-10-29 10:20:16 +02:00
|
|
|
|
2016-09-12 08:11:55 +02:00
|
|
|
class Meta:
|
|
|
|
model = TicketCC
|
2016-10-29 10:08:57 +02:00
|
|
|
exclude = ('ticket', 'user',)
|
2016-09-12 08:11:55 +02:00
|
|
|
|
2016-10-29 10:27:29 +02:00
|
|
|
|
2011-05-10 11:27:11 +02:00
|
|
|
class TicketDependencyForm(forms.ModelForm):
|
2016-10-29 10:08:57 +02:00
|
|
|
''' Adds a different ticket as a dependency for this Ticket '''
|
2016-10-23 22:09:17 +02:00
|
|
|
|
2011-05-10 11:27:11 +02:00
|
|
|
class Meta:
|
|
|
|
model = TicketDependency
|
2024-06-10 17:33:08 +02:00
|
|
|
fields = ('depends_on',)
|
2020-10-29 23:32:02 +01:00
|
|
|
|
2024-06-08 12:11:01 +02:00
|
|
|
def __init__(self, ticket, *args, **kwargs):
|
|
|
|
super(TicketDependencyForm,self).__init__(*args, **kwargs)
|
|
|
|
|
2024-06-08 18:49:53 +02:00
|
|
|
# Only open tickets except myself, existing dependencies and parents
|
|
|
|
self.fields['depends_on'].queryset = Ticket.objects.filter(status__in=Ticket.OPEN_STATUSES).exclude(id=ticket.id).exclude(depends_on__ticket=ticket).exclude(ticketdependency__depends_on=ticket)
|
2024-06-08 12:11:01 +02:00
|
|
|
|
2024-06-06 00:28:36 +02:00
|
|
|
class TicketResolvesForm(forms.ModelForm):
|
|
|
|
''' Adds this ticket as a dependency for a different ticket '''
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = TicketDependency
|
|
|
|
fields = ('ticket',)
|
|
|
|
|
2024-06-08 12:11:01 +02:00
|
|
|
def __init__(self, ticket, *args, **kwargs):
|
|
|
|
super(TicketResolvesForm,self).__init__(*args, **kwargs)
|
|
|
|
|
2024-06-08 18:49:53 +02:00
|
|
|
# Only open tickets except myself, existing dependencies and parents
|
|
|
|
self.fields['ticket'].queryset = Ticket.objects.exclude(status__in=Ticket.OPEN_STATUSES).exclude(id=ticket.id).exclude(depends_on__ticket=ticket).exclude(ticketdependency__depends_on=ticket)
|
2020-10-29 23:32:02 +01:00
|
|
|
|
|
|
|
|
|
|
|
class MultipleTicketSelectForm(forms.Form):
|
|
|
|
tickets = forms.ModelMultipleChoiceField(
|
|
|
|
label=_('Tickets to merge'),
|
2020-10-30 20:19:50 +01:00
|
|
|
queryset=Ticket.objects.filter(merged_to=None),
|
2020-10-29 23:32:02 +01:00
|
|
|
widget=forms.SelectMultiple(attrs={'class': 'form-control'})
|
|
|
|
)
|
|
|
|
|
|
|
|
def clean_tickets(self):
|
|
|
|
tickets = self.cleaned_data.get('tickets')
|
|
|
|
if len(tickets) < 2:
|
2020-10-30 20:19:50 +01:00
|
|
|
raise ValidationError(_('Please choose at least 2 tickets.'))
|
2020-10-29 23:32:02 +01:00
|
|
|
if len(tickets) > 4:
|
2022-07-12 12:34:19 +02:00
|
|
|
raise ValidationError(
|
|
|
|
_('Impossible to merge more than 4 tickets...'))
|
|
|
|
queues = tickets.order_by('queue').distinct(
|
|
|
|
).values_list('queue', flat=True)
|
2020-10-30 20:19:50 +01:00
|
|
|
if len(queues) != 1:
|
2022-07-12 12:34:19 +02:00
|
|
|
raise ValidationError(
|
|
|
|
_('All selected tickets must share the same queue in order to be merged.'))
|
2020-10-29 23:32:02 +01:00
|
|
|
return tickets
|
2023-04-23 00:36:10 +02:00
|
|
|
|
|
|
|
|
2023-04-30 01:05:46 +02:00
|
|
|
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))
|
|
|
|
|
|
|
|
|
2023-04-23 00:36:10 +02:00
|
|
|
class ChecklistForm(forms.ModelForm):
|
|
|
|
name = forms.CharField(
|
2023-04-29 00:24:06 +02:00
|
|
|
widget=forms.TextInput(attrs={'class': 'form-control'}),
|
|
|
|
required=True,
|
2023-04-23 00:36:10 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = Checklist
|
|
|
|
fields = ('name',)
|
|
|
|
|
2023-04-29 00:24:06 +02:00
|
|
|
|
|
|
|
class CreateChecklistForm(ChecklistForm):
|
|
|
|
checklist_template = forms.ModelChoiceField(
|
|
|
|
label=_("Template"),
|
|
|
|
queryset=ChecklistTemplate.objects.all(),
|
|
|
|
widget=forms.Select(attrs={'class': 'form-control'}),
|
|
|
|
required=False,
|
|
|
|
)
|
|
|
|
|
2023-04-30 03:11:27 +02:00
|
|
|
class Meta(ChecklistForm.Meta):
|
|
|
|
fields = ('checklist_template', 'name')
|
2023-04-29 00:24:06 +02:00
|
|
|
|
|
|
|
|
|
|
|
class FormControlDeleteFormSet(forms.BaseInlineFormSet):
|
|
|
|
deletion_widget = forms.CheckboxInput(attrs={'class': 'form-control'})
|