diff --git a/helpdesk/admin.py b/helpdesk/admin.py index cb5ffe5f..09bac9c1 100644 --- a/helpdesk/admin.py +++ b/helpdesk/admin.py @@ -2,6 +2,7 @@ from django.contrib import admin from helpdesk.models import Queue, Ticket, FollowUp, PreSetReply, KBCategory from helpdesk.models import EscalationExclusion, EmailTemplate, KBItem from helpdesk.models import TicketChange, Attachment, IgnoreEmail +from helpdesk.models import CustomField class QueueAdmin(admin.ModelAdmin): list_display = ('title', 'slug', 'email_address') @@ -23,6 +24,9 @@ class FollowUpAdmin(admin.ModelAdmin): class KBItemAdmin(admin.ModelAdmin): list_display = ('category', 'title', 'last_updated',) list_display_links = ('title',) + +class CustomFieldAdmin(admin.ModelAdmin): + list_display = ('name', 'label', 'data_type') admin.site.register(Ticket, TicketAdmin) admin.site.register(Queue, QueueAdmin) @@ -33,3 +37,4 @@ admin.site.register(EmailTemplate) admin.site.register(KBCategory) admin.site.register(KBItem, KBItemAdmin) admin.site.register(IgnoreEmail) +admin.site.register(CustomField, CustomFieldAdmin) diff --git a/helpdesk/forms.py b/helpdesk/forms.py index cde9618d..7f8ea27c 100644 --- a/helpdesk/forms.py +++ b/helpdesk/forms.py @@ -15,7 +15,7 @@ from django.contrib.auth.models import User from django.utils.translation import ugettext as _ from helpdesk.lib import send_templated_mail -from helpdesk.models import Ticket, Queue, FollowUp, Attachment, IgnoreEmail, TicketCC +from helpdesk.models import Ticket, Queue, FollowUp, Attachment, IgnoreEmail, TicketCC, CustomField, TicketCustomFieldValue from helpdesk.settings import HAS_TAG_SUPPORT class EditTicketForm(forms.ModelForm): @@ -93,6 +93,56 @@ class TicketForm(forms.Form): 'ticket'), ) + def __init__(self, *args, **kwargs): + """ + Add any custom fields that are defined to the form + """ + super(TicketForm, self).__init__(*args, **kwargs) + for field in CustomField.objects.all(): + instanceargs = { + 'label': field.label, + 'help_text': field.help_text, + 'required': field.required, + } + if field.data_type == 'varchar': + fieldclass = forms.CharField + instanceargs['max_length'] = field.max_length + elif field.data_type == 'text': + fieldclass = forms.CharField + instanceargs['widget'] = forms.Textarea + instanceargs['max_length'] = field.max_length + elif field.data_type == 'integer': + fieldclass = forms.IntegerField + elif field.data_type == 'decimal': + fieldclass = forms.DecimalField + instanceargs['decimal_places'] = field.decimal_places + instanceargs['max_digits'] = field.max_length + elif field.data_type == 'list': + fieldclass = forms.ChoiceField + choices = [] + for line in field.list_values.split("\n"): + choices.append((line, line)) + instanceargs['choices'] = choices + elif field.data_type == 'boolean': + fieldclass = forms.BooleanField + elif field.data_type == 'date': + fieldclass = forms.DateField + elif field.data_type == 'time': + fieldclass = forms.TimeField + elif field.data_type == 'datetime': + fieldclass = forms.DateTimeField + elif field.data_type == 'email': + fieldclass = forms.EmailField + elif field.data_type == 'url': + fieldclass = forms.URLField + elif field.data_type == 'ipaddress': + fieldclass = forms.IPAddressField + elif field.data_type == 'slug': + fieldclass = forms.SlugField + + self.fields['custom_%s' % field.name] = fieldclass(**instanceargs) + + def save(self, user): """ Writes and returns a Ticket() object @@ -119,6 +169,15 @@ class TicketForm(forms.Form): except User.DoesNotExist: t.assigned_to = None t.save() + + for field, value in self.cleaned_data.items(): + if field.startswith('custom_'): + field_name = field.replace('custom_', '') + customfield = CustomField.objects.get(name=field_name) + cfv = TicketCustomFieldValue(ticket=t, + field=customfield, + value=value) + cfv.save() f = FollowUp( ticket = t, title = _('Ticket Opened'), @@ -249,6 +308,54 @@ class PublicTicketForm(forms.Form): help_text=_('You can attach a file such as a document or screenshot to this ticket.'), ) + def __init__(self, *args, **kwargs): + """ + Add any custom fields that are defined to the form + """ + super(PublicTicketForm, self).__init__(*args, **kwargs) + for field in CustomField.objects.filter(staff_only=False): + instanceargs = { + 'label': field.label, + 'help_text': field.help_text, + 'required': field.required, + } + if field.data_type == 'varchar': + fieldclass = forms.CharField + instanceargs['max_length'] = field.max_length + elif field.data_type == 'text': + fieldclass = forms.TextField + instanceargs['max_length'] = field.max_length + elif field.data_type == 'integer': + fieldclass = forms.IntegerField + elif field.data_type == 'decimal': + fieldclass = forms.DecimalField + instanceargs['decimal_places'] = field.decimal_places + instanceargs['max_digits'] = field.max_length + elif field.data_type == 'list': + fieldclass = forms.ChoiceField + choices = () + for line in field.choices: + choices.append((line, line)) + instanceargs['choices'] = choices + elif field.data_type == 'boolean': + fieldclass = forms.BooleanField + elif field.data_type == 'date': + fieldclass = forms.DateField + elif field.data_type == 'time': + fieldclass = forms.TimeField + elif field.data_type == 'datetime': + fieldclass = forms.DateTimeField + elif field.data_type == 'email': + fieldclass = forms.EmailField + elif field.data_type == 'url': + fieldclass = forms.URLField + elif field.data_type == 'ipaddress': + fieldclass = forms.IPAddressField + elif field.data_type == 'slug': + fieldclass = forms.SlugField + + self.fields['custom_%s' % field.name] = fieldclass(**instanceargs) + def save(self): """ Writes and returns a Ticket() object @@ -268,6 +375,15 @@ class PublicTicketForm(forms.Form): t.save() + for field, value in self.cleaned_data.items(): + if field.startswith('custom_'): + field_name = field.replace('custom_', '') + customfield = CustomField.objects.get(name=field_name) + cfv = TicketCustomFieldValue(ticket=t, + field=customfield, + value=value) + cfv.save() + f = FollowUp( ticket = t, title = _('Ticket Opened Via Web'), diff --git a/helpdesk/models.py b/helpdesk/models.py index 3b40091f..52df8d78 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -1085,3 +1085,102 @@ class TicketCC(models.Model): def __unicode__(self): return u'%s for %s' % (self.display, self.ticket.title) + + +class CustomField(models.Model): + """ + Definitions for custom fields that are glued onto each ticket. + """ + + name = models.SlugField( + _('Field Name'), + help_text=_('As used in the database and behind the scenes. Must be unique and consist of only lowercase letters with no punctuation.'), + unique=True, + ) + + label = models.CharField( + _('Label'), + max_length='30', + help_text=_('The display label for this field'), + ) + + help_text = models.TextField( + _('Help Text'), + help_text=_('Shown to the user when editing the ticket'), + ) + + DATA_TYPE_CHOICES = ( + ('varchar', _('Character (single line)')), + ('text', _('Text (multi-line)')), + ('integer', _('Integer')), + ('decimal', _('Decimal')), + ('list', _('List')), + ('boolean', _('Boolean (checkbox yes/no)')), + ('date', _('Date')), + ('time', _('Time')), + ('datetime', _('Date & Time')), + ('email', _('E-Mail Address')), + ('url', _('URL')), + ('ipaddress', _('IP Address')), + ('slug', _('Slug')), + ) + + data_type = models.CharField( + _('Data Type'), + max_length=100, + help_text=_('Allows you to restrict the data entered into this field'), + choices=DATA_TYPE_CHOICES, + ) + + max_length = models.IntegerField( + _('Maximum Length (characters)'), + blank=True, + null=True, + ) + + decimal_places = models.IntegerField( + _('Decimal Places'), + help_text=_('Only used for decimal fields'), + blank=True, + null=True, + ) + + list_values = models.TextField( + _('List Values'), + help_text=_('For list fields only. Enter one option per line.'), + blank=True, + null=True, + ) + + required = models.BooleanField( + _('Required?'), + help_text=_('Does the user have to enter a value for this field?'), + ) + + staff_only = models.BooleanField( + _('Staff Only?'), + help_text=_('If this is ticked, then the public submission form will NOT show this field'), + ) + + def __unicode__(self): + return '%s' % (self.name) + + +class TicketCustomFieldValue(models.Model): + ticket = models.ForeignKey( + Ticket, + verbose_name=_('Ticket'), + ) + + field = models.ForeignKey( + CustomField, + verbose_name=_('Field'), + ) + + value = models.TextField(blank=True, null=True) + + def __unicode__(self): + return '%s / %s' % (self.ticket, self.field) + + class Meta: + unique_together = ('ticket', 'field'), diff --git a/helpdesk/templates/helpdesk/public_view_ticket.html b/helpdesk/templates/helpdesk/public_view_ticket.html index 526a83f3..9fb0de67 100644 --- a/helpdesk/templates/helpdesk/public_view_ticket.html +++ b/helpdesk/templates/helpdesk/public_view_ticket.html @@ -1,4 +1,5 @@ {% extends "helpdesk/public_base.html" %}{% load i18n %} +{% cycle 'row_odd' 'row_even' as rowcolors %} {% block helpdesk_title %}{% trans "View a Ticket" %}{% endblock %} {% block helpdesk_body %} @@ -7,39 +8,45 @@
{{ ticket.id }}. {{ ticket.title }} [{{ ticket.get_status }}] | {% if ticket.on_hold %}{% trans "Unhold" %}{% else %}{% trans "Hold" %}{% endif %} |
{% blocktrans with ticket.queue as queue %}Queue: {{ queue }}{% endblocktrans %} | |
---|---|
{% trans "Submitted On" %} | {{ ticket.created|date:"r" }} ({{ ticket.created|timesince }} ago) |
{% trans "Assigned To" %} | {{ ticket.get_assigned_to }}{% ifequal ticket.get_assigned_to _('Unassigned') %} {% endifequal %} |
{% trans "Submitter E-Mail" %} | {{ ticket.submitter_email }}{% if user.is_superuser %} {% trans "Ignore" %}{% endif %} |
{% trans "Priority" %} | {{ ticket.get_priority_display }} |
{% trans "Copies To" %} | {% for ticketcc in ticket.ticketcc_set.all %}{{ ticketcc.display }}{% if not forloop.last %}, {% endif %}{% endfor %} {% trans "Manage" %} |
{% trans "Tags" %} | {{ ticket.tags }} |
{{ customfield.field.label }} | +{{ customfield.value }} | +
{% trans "Description" %} | |
{{ ticket.description|force_escape|urlizetrunc:50|linebreaksbr }} | |
{% trans "Resolution" %}{% ifequal ticket.get_status_display "Resolved" %} {% endifequal %} | |
{{ ticket.resolution|force_escape|urlizetrunc:50|linebreaksbr }} |