forked from extern/django-helpdesk
Initial shot at custom fields on Ticket model. Manage these fields via Django admin.
This commit is contained in:
parent
723b4f1881
commit
ebe0382725
@ -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)
|
||||
|
@ -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'),
|
||||
|
@ -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'),
|
||||
|
@ -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 @@
|
||||
<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_odd'>
|
||||
<tr class='{% cycle rowcolors %}'>
|
||||
<th>{% trans "Submitted On" %}</th>
|
||||
<td>{{ ticket.created|date:"r" }} ({{ ticket.created|timesince }} ago)</td>
|
||||
</tr>
|
||||
|
||||
<tr class='row_even'>
|
||||
<tr class='{% cycle rowcolors %}'>
|
||||
<th>{% trans "Submitter E-Mail" %}</th>
|
||||
<td>{{ ticket.submitter_email }}</td>
|
||||
</tr>
|
||||
|
||||
<tr class='row_odd'>
|
||||
<tr class='{% cycle rowcolors %}'>
|
||||
<th>{% trans "Priority" %}</th>
|
||||
<td>{{ ticket.get_priority_display }}</td>
|
||||
</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 %}
|
||||
<tr class='row_even'>
|
||||
<tr class='{% cycle rowcolors %}'>
|
||||
<th>{% trans "Tags" %}</th>
|
||||
<td>{{ ticket.tags }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
<tr class='row_{% if tags_enabled %}odd{% else %}even{% endif %}'>
|
||||
<tr class='{% cycle rowcolors %}'>
|
||||
<th colspan='2'>{% trans "Description" %}</th>
|
||||
</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>
|
||||
</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>
|
||||
</tr>
|
||||
<tr class='row_{% if tags_enabled %}even{% else %}odd{% endif %}'>
|
||||
<tr class='{% cycle rowcolors %}'>
|
||||
<td colspan='2'>{{ ticket.resolution|urlizetrunc:50|linebreaksbr }}</td>
|
||||
</tr>{% endif %}
|
||||
|
||||
|
@ -1,52 +1,58 @@
|
||||
{% load i18n %}
|
||||
{% cycle 'row_odd' 'row_even' as rowcolors %}
|
||||
<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_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>
|
||||
<td>{{ ticket.created|date:"r" }} ({{ ticket.created|timesince }} ago)</td>
|
||||
</tr>
|
||||
|
||||
<tr class='row_even'>
|
||||
<tr class='{% cycle rowcolors %}'>
|
||||
<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>
|
||||
</tr>
|
||||
|
||||
<tr class='row_odd'>
|
||||
<tr class='{% cycle rowcolors %}'>
|
||||
<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>
|
||||
</tr>
|
||||
|
||||
<tr class='row_even'>
|
||||
<tr class='{% cycle rowcolors %}'>
|
||||
<th>{% trans "Priority" %}</th>
|
||||
<td>{{ ticket.get_priority_display }}</td>
|
||||
</tr>
|
||||
|
||||
<tr class='row_odd'>
|
||||
<tr class='{% cycle rowcolors %}'>
|
||||
<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>
|
||||
</tr>
|
||||
|
||||
{% if tags_enabled %}
|
||||
<tr class='row_even'>
|
||||
<tr class='{% cycle rowcolors %}'>
|
||||
<th>{% trans "Tags" %}</th>
|
||||
<td>{{ ticket.tags }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
<tr class='row_even'>
|
||||
<tr class='row_{% if tags_enabled %}odd{% else %}even{% endif %}'>
|
||||
{% for customfield in ticket.ticketcustomfieldvalue_set.all %}
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</tr>{% endif %}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user