Initial shot at custom fields on Ticket model. Manage these fields via Django admin.

This commit is contained in:
Ross Poulton 2011-02-02 11:22:46 +00:00
parent 723b4f1881
commit ebe0382725
5 changed files with 253 additions and 20 deletions

View File

@ -2,6 +2,7 @@ from django.contrib import admin
from helpdesk.models import Queue, Ticket, FollowUp, PreSetReply, KBCategory from helpdesk.models import Queue, Ticket, FollowUp, PreSetReply, KBCategory
from helpdesk.models import EscalationExclusion, EmailTemplate, KBItem from helpdesk.models import EscalationExclusion, EmailTemplate, KBItem
from helpdesk.models import TicketChange, Attachment, IgnoreEmail from helpdesk.models import TicketChange, Attachment, IgnoreEmail
from helpdesk.models import CustomField
class QueueAdmin(admin.ModelAdmin): class QueueAdmin(admin.ModelAdmin):
list_display = ('title', 'slug', 'email_address') list_display = ('title', 'slug', 'email_address')
@ -23,6 +24,9 @@ class FollowUpAdmin(admin.ModelAdmin):
class KBItemAdmin(admin.ModelAdmin): class KBItemAdmin(admin.ModelAdmin):
list_display = ('category', 'title', 'last_updated',) list_display = ('category', 'title', 'last_updated',)
list_display_links = ('title',) list_display_links = ('title',)
class CustomFieldAdmin(admin.ModelAdmin):
list_display = ('name', 'label', 'data_type')
admin.site.register(Ticket, TicketAdmin) admin.site.register(Ticket, TicketAdmin)
admin.site.register(Queue, QueueAdmin) admin.site.register(Queue, QueueAdmin)
@ -33,3 +37,4 @@ admin.site.register(EmailTemplate)
admin.site.register(KBCategory) admin.site.register(KBCategory)
admin.site.register(KBItem, KBItemAdmin) admin.site.register(KBItem, KBItemAdmin)
admin.site.register(IgnoreEmail) admin.site.register(IgnoreEmail)
admin.site.register(CustomField, CustomFieldAdmin)

View File

@ -15,7 +15,7 @@ from django.contrib.auth.models import User
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from helpdesk.lib import send_templated_mail 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 from helpdesk.settings import HAS_TAG_SUPPORT
class EditTicketForm(forms.ModelForm): class EditTicketForm(forms.ModelForm):
@ -93,6 +93,56 @@ class TicketForm(forms.Form):
'ticket'), '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): def save(self, user):
""" """
Writes and returns a Ticket() object Writes and returns a Ticket() object
@ -119,6 +169,15 @@ class TicketForm(forms.Form):
except User.DoesNotExist: except User.DoesNotExist:
t.assigned_to = None t.assigned_to = None
t.save() 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, f = FollowUp( ticket = t,
title = _('Ticket Opened'), 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.'), 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): def save(self):
""" """
Writes and returns a Ticket() object Writes and returns a Ticket() object
@ -268,6 +375,15 @@ class PublicTicketForm(forms.Form):
t.save() 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( f = FollowUp(
ticket = t, ticket = t,
title = _('Ticket Opened Via Web'), title = _('Ticket Opened Via Web'),

View File

@ -1085,3 +1085,102 @@ class TicketCC(models.Model):
def __unicode__(self): def __unicode__(self):
return u'%s for %s' % (self.display, self.ticket.title) 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'),

View File

@ -1,4 +1,5 @@
{% extends "helpdesk/public_base.html" %}{% load i18n %} {% extends "helpdesk/public_base.html" %}{% load i18n %}
{% cycle 'row_odd' 'row_even' as rowcolors %}
{% block helpdesk_title %}{% trans "View a Ticket" %}{% endblock %} {% block helpdesk_title %}{% trans "View a Ticket" %}{% endblock %}
{% block helpdesk_body %} {% block helpdesk_body %}
@ -7,39 +8,45 @@
<tr class='row_tablehead'><td colspan='2'>{{ ticket.id }}. {{ ticket.title }} [{{ ticket.get_status }}]</td></tr> <tr class='row_tablehead'><td colspan='2'>{{ ticket.id }}. {{ ticket.title }} [{{ ticket.get_status }}]</td></tr>
<tr class='row_columnheads'><th colspan='2'>{% blocktrans with ticket.queue as queue_name %}Queue: {{ queue_name }}{% endblocktrans %}</th></tr> <tr class='row_columnheads'><th colspan='2'>{% blocktrans with ticket.queue as queue_name %}Queue: {{ queue_name }}{% endblocktrans %}</th></tr>
<tr class='row_odd'> <tr class='{% cycle rowcolors %}'>
<th>{% trans "Submitted On" %}</th> <th>{% trans "Submitted On" %}</th>
<td>{{ ticket.created|date:"r" }} ({{ ticket.created|timesince }} ago)</td> <td>{{ ticket.created|date:"r" }} ({{ ticket.created|timesince }} ago)</td>
</tr> </tr>
<tr class='row_even'> <tr class='{% cycle rowcolors %}'>
<th>{% trans "Submitter E-Mail" %}</th> <th>{% trans "Submitter E-Mail" %}</th>
<td>{{ ticket.submitter_email }}</td> <td>{{ ticket.submitter_email }}</td>
</tr> </tr>
<tr class='row_odd'> <tr class='{% cycle rowcolors %}'>
<th>{% trans "Priority" %}</th> <th>{% trans "Priority" %}</th>
<td>{{ ticket.get_priority_display }}</td> <td>{{ ticket.get_priority_display }}</td>
</tr> </tr>
{% for customfield in ticket.ticketcustomfieldvalue_set.all %}
<tr class='{% cycle rowcolors %}'>
<th>{{ customfield.field.label }}</th>
<td>{{ customfield.value }}</td>
</tr>{% endfor %}
{% if tags_enabled %} {% if tags_enabled %}
<tr class='row_even'> <tr class='{% cycle rowcolors %}'>
<th>{% trans "Tags" %}</th> <th>{% trans "Tags" %}</th>
<td>{{ ticket.tags }}</td> <td>{{ ticket.tags }}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr class='row_{% if tags_enabled %}odd{% else %}even{% endif %}'> <tr class='{% cycle rowcolors %}'>
<th colspan='2'>{% trans "Description" %}</th> <th colspan='2'>{% trans "Description" %}</th>
</tr> </tr>
<tr class='row_{% if tags_enabled %}odd{% else %}even{% endif %}'> <tr class='{% cycle rowcolors %}'>
<td colspan='2'>{{ ticket.description|force_escape|urlizetrunc:50|linebreaksbr }}</td> <td colspan='2'>{{ ticket.description|force_escape|urlizetrunc:50|linebreaksbr }}</td>
</tr> </tr>
{% if ticket.resolution %}<tr class='row_{% if tags_enabled %}even{% else %}odd{% endif %}'> {% if ticket.resolution %}<tr class='{% cycle rowcolors %}'>
<th colspan='2'>{% trans "Resolution" %}{% ifequal ticket.get_status_display "Resolved" %} <a href='{{ request.get_full_path }}&close'><img src='{{ MEDIA_URL }}/helpdesk/buttons/accept.png' alt='{% trans "Accept" %}' title='{% trans "Accept and Close" %}' width='60' height='15' /></a>{% endifequal %}</th> <th colspan='2'>{% trans "Resolution" %}{% ifequal ticket.get_status_display "Resolved" %} <a href='{{ request.get_full_path }}&close'><img src='{{ MEDIA_URL }}/helpdesk/buttons/accept.png' alt='{% trans "Accept" %}' title='{% trans "Accept and Close" %}' width='60' height='15' /></a>{% endifequal %}</th>
</tr> </tr>
<tr class='row_{% if tags_enabled %}even{% else %}odd{% endif %}'> <tr class='{% cycle rowcolors %}'>
<td colspan='2'>{{ ticket.resolution|urlizetrunc:50|linebreaksbr }}</td> <td colspan='2'>{{ ticket.resolution|urlizetrunc:50|linebreaksbr }}</td>
</tr>{% endif %} </tr>{% endif %}

View File

@ -1,52 +1,58 @@
{% load i18n %} {% load i18n %}
{% cycle 'row_odd' 'row_even' as rowcolors %}
<table width='100%'> <table width='100%'>
<tr class='row_tablehead'><td>{{ ticket.id }}. {{ ticket.title }} [{{ ticket.get_status }}]</td><td align='right'><a href='{% url helpdesk_edit ticket.id %}'><img src='{{ MEDIA_URL }}helpdesk/buttons/edit.png' alt='Edit' title='Edit' width='60' height='15' /></a><a href='{% url helpdesk_delete ticket.id %}'><img src='{{ MEDIA_URL }}helpdesk/buttons/delete.png' alt='Delete' title='Delete' width='60' height='15' /></a>{% if ticket.on_hold %}<a href='unhold/'>{% trans "Unhold" %}</a>{% else %}<a href='hold/'>{% trans "Hold" %}</a>{% endif %}</td></tr> <tr class='row_tablehead'><td>{{ ticket.id }}. {{ ticket.title }} [{{ ticket.get_status }}]</td><td align='right'><a href='{% url helpdesk_edit ticket.id %}'><img src='{{ MEDIA_URL }}helpdesk/buttons/edit.png' alt='Edit' title='Edit' width='60' height='15' /></a><a href='{% url helpdesk_delete ticket.id %}'><img src='{{ MEDIA_URL }}helpdesk/buttons/delete.png' alt='Delete' title='Delete' width='60' height='15' /></a>{% if ticket.on_hold %}<a href='unhold/'>{% trans "Unhold" %}</a>{% else %}<a href='hold/'>{% trans "Hold" %}</a>{% endif %}</td></tr>
<tr class='row_columnheads'><th colspan='2'>{% blocktrans with ticket.queue as queue %}Queue: {{ queue }}{% endblocktrans %}</th></tr> <tr class='row_columnheads'><th colspan='2'>{% blocktrans with ticket.queue as queue %}Queue: {{ queue }}{% endblocktrans %}</th></tr>
<tr class='row_odd'> <tr class='{% cycle rowcolors %}'>
<th>{% trans "Submitted On" %}</th> <th>{% trans "Submitted On" %}</th>
<td>{{ ticket.created|date:"r" }} ({{ ticket.created|timesince }} ago)</td> <td>{{ ticket.created|date:"r" }} ({{ ticket.created|timesince }} ago)</td>
</tr> </tr>
<tr class='row_even'> <tr class='{% cycle rowcolors %}'>
<th>{% trans "Assigned To" %}</th> <th>{% trans "Assigned To" %}</th>
<td>{{ ticket.get_assigned_to }}{% ifequal ticket.get_assigned_to _('Unassigned') %} <strong><a href='?take'><span class='button button_take'>{% trans "Take" %}</span></a></strong>{% endifequal %}</td> <td>{{ ticket.get_assigned_to }}{% ifequal ticket.get_assigned_to _('Unassigned') %} <strong><a href='?take'><span class='button button_take'>{% trans "Take" %}</span></a></strong>{% endifequal %}</td>
</tr> </tr>
<tr class='row_odd'> <tr class='{% cycle rowcolors %}'>
<th>{% trans "Submitter E-Mail" %}</th> <th>{% trans "Submitter E-Mail" %}</th>
<td>{{ ticket.submitter_email }}{% if user.is_superuser %} <strong><a href='{% url helpdesk_email_ignore_add %}?email={{ ticket.submitter_email }}'>{% trans "Ignore" %}</a></strong>{% endif %}</td> <td>{{ ticket.submitter_email }}{% if user.is_superuser %} <strong><a href='{% url helpdesk_email_ignore_add %}?email={{ ticket.submitter_email }}'>{% trans "Ignore" %}</a></strong>{% endif %}</td>
</tr> </tr>
<tr class='row_even'> <tr class='{% cycle rowcolors %}'>
<th>{% trans "Priority" %}</th> <th>{% trans "Priority" %}</th>
<td>{{ ticket.get_priority_display }}</td> <td>{{ ticket.get_priority_display }}</td>
</tr> </tr>
<tr class='row_odd'> <tr class='{% cycle rowcolors %}'>
<th>{% trans "Copies To" %}</th> <th>{% trans "Copies To" %}</th>
<td>{% for ticketcc in ticket.ticketcc_set.all %}{{ ticketcc.display }}{% if not forloop.last %}, {% endif %}{% endfor %} <strong><a href='{% url helpdesk_ticket_cc ticket.id %}'>{% trans "Manage" %}</a></strong></td> <td>{% for ticketcc in ticket.ticketcc_set.all %}{{ ticketcc.display }}{% if not forloop.last %}, {% endif %}{% endfor %} <strong><a href='{% url helpdesk_ticket_cc ticket.id %}'>{% trans "Manage" %}</a></strong></td>
</tr> </tr>
{% if tags_enabled %} {% if tags_enabled %}
<tr class='row_even'> <tr class='{% cycle rowcolors %}'>
<th>{% trans "Tags" %}</th> <th>{% trans "Tags" %}</th>
<td>{{ ticket.tags }}</td> <td>{{ ticket.tags }}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr class='row_even'> {% for customfield in ticket.ticketcustomfieldvalue_set.all %}
<tr class='row_{% if tags_enabled %}odd{% else %}even{% endif %}'> <tr class='{% cycle rowcolors %}'>
<th>{{ customfield.field.label }}</th>
<td>{{ customfield.value }}</td>
</tr>{% endfor %}
<tr class='{% cycle rowcolors %}'>
<th colspan='2'>{% trans "Description" %}</th> <th colspan='2'>{% trans "Description" %}</th>
</tr> </tr>
<tr class='row_{% if tags_enabled %}even{% else %}odd{% endif %}'> <tr class='{% cycle rowcolors %}'>
<td colspan='2'>{{ ticket.description|force_escape|urlizetrunc:50|linebreaksbr }}</td> <td colspan='2'>{{ ticket.description|force_escape|urlizetrunc:50|linebreaksbr }}</td>
</tr> </tr>
{% if ticket.resolution %}<tr class='row_{% if tags_enabled %}odd{% else %}even{% endif %}'> {% if ticket.resolution %}<tr class='{% cycle rowcolors %}'>
<th colspan='2'>{% trans "Resolution" %}{% ifequal ticket.get_status_display "Resolved" %} <a href='?close'><img src='{{ MEDIA_URL }}helpdesk/buttons/accept.png' alt='{% trans "Accept" %}' title='{% trans "Accept and Close" %}' width='60' height='15' /></a>{% endifequal %}</th> <th colspan='2'>{% trans "Resolution" %}{% ifequal ticket.get_status_display "Resolved" %} <a href='?close'><img src='{{ MEDIA_URL }}helpdesk/buttons/accept.png' alt='{% trans "Accept" %}' title='{% trans "Accept and Close" %}' width='60' height='15' /></a>{% endifequal %}</th>
</tr> </tr>
<tr class='row_{% if tags_enabled %}even{% else %}odd{% endif %}'> <tr class='{% cycle rowcolors %}'>
<td colspan='2'>{{ ticket.resolution|force_escape|urlizetrunc:50|linebreaksbr }}</td> <td colspan='2'>{{ ticket.resolution|force_escape|urlizetrunc:50|linebreaksbr }}</td>
</tr>{% endif %} </tr>{% endif %}