forked from extern/django-helpdesk
Merge pull request #797 from auto-mat/iframe_submit
Iframe submit, redesigned KB, various bug fixes
This commit is contained in:
commit
6e5e8cb21f
@ -7,5 +7,12 @@ Django-helpdesk associates an email address with each submitted ticket. If you i
|
|||||||
- `title`
|
- `title`
|
||||||
- `body`
|
- `body`
|
||||||
- `submitter_email`
|
- `submitter_email`
|
||||||
|
- `custom_<custom-field-slug>`
|
||||||
|
|
||||||
|
There is also a page under the url `/tickets/submit_iframe/` with the same behavior.
|
||||||
|
|
||||||
|
Fields may be hidden by adding them to a comma separated `_hide_fieds_` query parameter.
|
||||||
|
|
||||||
|
Here is an example url to get you started: `http://localhost:8000/desk/tickets/submit_iframe/?queue=1;custom_dpnk-user=http://lol.cz;submitter_email=foo@bar.cz;title=lol;_hide_fields_=title,queue,submitter_email`. This url sets the queue to 1, sets the custom field `dpnk-url` to `http://lol.cz` and submitter_email to `lol@baz.cz` and hides the title, queue, and submitter_email fields. Note that hidden fields should be set to a default.
|
||||||
|
|
||||||
Note that these fields will continue to be user-editable despite being pre-filled.
|
Note that these fields will continue to be user-editable despite being pre-filled.
|
||||||
|
@ -72,7 +72,7 @@ class FollowUpAdmin(admin.ModelAdmin):
|
|||||||
class KBItemAdmin(admin.ModelAdmin):
|
class KBItemAdmin(admin.ModelAdmin):
|
||||||
list_display = ('category', 'title', 'last_updated',)
|
list_display = ('category', 'title', 'last_updated',)
|
||||||
inlines = [KBIAttachmentInline]
|
inlines = [KBIAttachmentInline]
|
||||||
readonly_fields = ('voted_by',)
|
readonly_fields = ('voted_by', 'downvoted_by')
|
||||||
|
|
||||||
list_display_links = ('title',)
|
list_display_links = ('title',)
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ from django.utils import timezone
|
|||||||
|
|
||||||
from helpdesk.lib import safe_template_context, process_attachments
|
from helpdesk.lib import safe_template_context, process_attachments
|
||||||
from helpdesk.models import (Ticket, Queue, FollowUp, IgnoreEmail, TicketCC,
|
from helpdesk.models import (Ticket, Queue, FollowUp, IgnoreEmail, TicketCC,
|
||||||
CustomField, TicketCustomFieldValue, TicketDependency, UserSettings)
|
CustomField, TicketCustomFieldValue, TicketDependency, UserSettings, KBItem)
|
||||||
from helpdesk import settings as helpdesk_settings
|
from helpdesk import settings as helpdesk_settings
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
@ -177,6 +177,16 @@ class AbstractTicketForm(CustomFieldMixin, 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, kbcategory=None, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if kbcategory:
|
||||||
|
self.fields['kbitem'] = forms.ChoiceField(
|
||||||
|
widget=forms.Select(attrs={'class': 'form-control'}),
|
||||||
|
required=False,
|
||||||
|
label=_('Knowedge Base Item'),
|
||||||
|
choices=[(kbi.pk, kbi.title) for kbi in KBItem.objects.filter(category=kbcategory.pk)],
|
||||||
|
)
|
||||||
|
|
||||||
def _add_form_custom_fields(self, staff_only_filter=None):
|
def _add_form_custom_fields(self, staff_only_filter=None):
|
||||||
if staff_only_filter is None:
|
if staff_only_filter is None:
|
||||||
queryset = CustomField.objects.all()
|
queryset = CustomField.objects.all()
|
||||||
@ -197,7 +207,10 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
|||||||
return Queue.objects.get(id=int(self.cleaned_data['queue']))
|
return Queue.objects.get(id=int(self.cleaned_data['queue']))
|
||||||
|
|
||||||
def _create_ticket(self):
|
def _create_ticket(self):
|
||||||
queue = self._get_queue()
|
queue = Queue.objects.get(id=int(self.cleaned_data['queue']))
|
||||||
|
kbitem = None
|
||||||
|
if 'kbitem' in self.cleaned_data:
|
||||||
|
kbitem = KBItem.objects.get(id=int(self.cleaned_data['kbitem']))
|
||||||
|
|
||||||
ticket = Ticket(title=self.cleaned_data['title'],
|
ticket = Ticket(title=self.cleaned_data['title'],
|
||||||
submitter_email=self.cleaned_data['submitter_email'],
|
submitter_email=self.cleaned_data['submitter_email'],
|
||||||
@ -207,6 +220,7 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
|||||||
description=self.cleaned_data['body'],
|
description=self.cleaned_data['body'],
|
||||||
priority=self.cleaned_data['priority'],
|
priority=self.cleaned_data['priority'],
|
||||||
due_date=self.cleaned_data['due_date'],
|
due_date=self.cleaned_data['due_date'],
|
||||||
|
kbitem=kbitem,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ticket, queue
|
return ticket, queue
|
||||||
@ -337,34 +351,28 @@ class PublicTicketForm(AbstractTicketForm):
|
|||||||
help_text=_('We will e-mail you when your ticket is updated.'),
|
help_text=_('We will e-mail you when your ticket is updated.'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, hidden_fields=(), readonly_fields=(), *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Add any (non-staff) custom fields that are defined to the form
|
Add any (non-staff) custom fields that are defined to the form
|
||||||
"""
|
"""
|
||||||
super(PublicTicketForm, self).__init__(*args, **kwargs)
|
super(PublicTicketForm, self).__init__(*args, **kwargs)
|
||||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE'):
|
self._add_form_custom_fields(False)
|
||||||
del self.fields['queue']
|
|
||||||
else:
|
|
||||||
self.fields['queue'].choices = [
|
|
||||||
('', '--------')
|
|
||||||
] + [
|
|
||||||
(q.id, q.title) for q in Queue.objects.filter(allow_public_submission=True)
|
|
||||||
]
|
|
||||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'):
|
|
||||||
self.fields['priority'].widget = forms.HiddenInput()
|
|
||||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_DUE_DATE'):
|
|
||||||
self.fields['due_date'].widget = forms.HiddenInput()
|
|
||||||
|
|
||||||
def _get_queue(self):
|
field_hide_table = {
|
||||||
if getattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE', None):
|
'queue': 'HELPDESK_PUBLIC_TICKET_QUEUE',
|
||||||
# force queue to be the pre-defined one
|
'priority': 'HELPDESK_PUBLIC_TICKET_PRIORITY',
|
||||||
# (only for anon submissions)
|
'due_date': 'HELPDESK_PUBLIC_TICKET_DUE_DATE',
|
||||||
return Queue.objects.filter(
|
}
|
||||||
slug=settings.HELPDESK_PUBLIC_TICKET_QUEUE
|
|
||||||
).first()
|
for field in self.fields.keys():
|
||||||
else:
|
setting = field_hide_table.get(field, None)
|
||||||
# get the queue user entered
|
if (setting and hasattr(settings, setting)) or field in hidden_fields:
|
||||||
return Queue.objects.get(id=int(self.cleaned_data['queue']))
|
self.fields[field].widget = forms.HiddenInput()
|
||||||
|
if field in readonly_fields:
|
||||||
|
self.fields[field].disabled = True
|
||||||
|
|
||||||
|
self.fields['queue'].choices = [('', '--------')] + [
|
||||||
|
(q.id, q.title) for q in Queue.objects.filter(allow_public_submission=True)]
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""
|
"""
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
71
helpdesk/migrations/0027_auto_20200107_1221.py
Normal file
71
helpdesk/migrations/0027_auto_20200107_1221.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# Generated by Django 2.2.6 on 2020-01-07 12:21
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('helpdesk', '0026_kbitem_attachments'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='kbcategory',
|
||||||
|
name='queue',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='helpdesk.Queue', verbose_name='Default queue when creating a ticket after viewing this category.'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='kbitem',
|
||||||
|
name='downvoted_by',
|
||||||
|
field=models.ManyToManyField(related_name='downvotes', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='kbitem',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='helpdesk.KBItem', verbose_name='Knowledge base item the user was viewing when they created this ticket.'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='followupattachment',
|
||||||
|
name='filename',
|
||||||
|
field=models.CharField(blank=True, max_length=1000, verbose_name='Filename'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='followupattachment',
|
||||||
|
name='mime_type',
|
||||||
|
field=models.CharField(blank=True, max_length=255, verbose_name='MIME Type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='followupattachment',
|
||||||
|
name='size',
|
||||||
|
field=models.IntegerField(blank=True, help_text='Size of this file in bytes', verbose_name='Size'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='kbiattachment',
|
||||||
|
name='filename',
|
||||||
|
field=models.CharField(blank=True, max_length=1000, verbose_name='Filename'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='kbiattachment',
|
||||||
|
name='mime_type',
|
||||||
|
field=models.CharField(blank=True, max_length=255, verbose_name='MIME Type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='kbiattachment',
|
||||||
|
name='size',
|
||||||
|
field=models.IntegerField(blank=True, help_text='Size of this file in bytes', verbose_name='Size'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='kbitem',
|
||||||
|
name='voted_by',
|
||||||
|
field=models.ManyToManyField(related_name='votes', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='queue',
|
||||||
|
name='enable_notifications_on_email_events',
|
||||||
|
field=models.BooleanField(blank=True, default=False, help_text='When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature', verbose_name='Notify contacts when email updates arrive'),
|
||||||
|
),
|
||||||
|
]
|
@ -559,6 +559,14 @@ class Ticket(models.Model):
|
|||||||
default=mk_secret,
|
default=mk_secret,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
kbitem = models.ForeignKey(
|
||||||
|
"KBItem",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name=_('Knowledge base item the user was viewing when they created this ticket.'),
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def time_spent(self):
|
def time_spent(self):
|
||||||
"""Return back total time spent on the ticket. This is calculated value
|
"""Return back total time spent on the ticket. This is calculated value
|
||||||
@ -605,6 +613,8 @@ class Ticket(models.Model):
|
|||||||
if dont_send_to is not None:
|
if dont_send_to is not None:
|
||||||
recipients.update(dont_send_to)
|
recipients.update(dont_send_to)
|
||||||
|
|
||||||
|
recipients.add(self.queue.email_address)
|
||||||
|
|
||||||
def should_receive(email):
|
def should_receive(email):
|
||||||
return email and email not in recipients
|
return email and email not in recipients
|
||||||
|
|
||||||
@ -1216,6 +1226,14 @@ class KBCategory(models.Model):
|
|||||||
_('Description'),
|
_('Description'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
queue = models.ForeignKey(
|
||||||
|
Queue,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name=_('Default queue when creating a ticket after viewing this category.'),
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s' % self.title
|
return '%s' % self.title
|
||||||
|
|
||||||
@ -1234,7 +1252,14 @@ class KBItem(models.Model):
|
|||||||
An item within the knowledgebase. Very straightforward question/answer
|
An item within the knowledgebase. Very straightforward question/answer
|
||||||
style system.
|
style system.
|
||||||
"""
|
"""
|
||||||
voted_by = models.ManyToManyField(settings.AUTH_USER_MODEL)
|
voted_by = models.ManyToManyField(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
related_name='votes',
|
||||||
|
)
|
||||||
|
downvoted_by = models.ManyToManyField(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
related_name='downvotes',
|
||||||
|
)
|
||||||
category = models.ForeignKey(
|
category = models.ForeignKey(
|
||||||
KBCategory,
|
KBCategory,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@ -1294,7 +1319,14 @@ class KBItem(models.Model):
|
|||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
return reverse('helpdesk:kb_item', args=(self.id,))
|
return str(reverse('helpdesk:kb_category', args=(self.category.slug,))) + "?kbitem=" + str(self.pk)
|
||||||
|
|
||||||
|
def query_url(self):
|
||||||
|
from django.urls import reverse
|
||||||
|
return str(reverse('helpdesk:list')) + "?kbitem=" + str(self.pk)
|
||||||
|
|
||||||
|
def num_open_tickets(self):
|
||||||
|
return Ticket.objects.filter(kbitem=self, status__in=(1, 2)).count()
|
||||||
|
|
||||||
def get_markdown(self):
|
def get_markdown(self):
|
||||||
return get_markdown(self.answer)
|
return get_markdown(self.answer)
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.urls import reverse
|
||||||
from model_utils import Choices
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from model_utils import Choices
|
||||||
|
|
||||||
|
from helpdesk.serializers import DatatablesTicketSerializer
|
||||||
|
|
||||||
|
|
||||||
def query_to_base64(query):
|
def query_to_base64(query):
|
||||||
"""
|
"""
|
||||||
@ -47,60 +51,31 @@ def query_to_dict(results, descriptions):
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
def apply_query(queryset, params):
|
def get_search_filter_args(search):
|
||||||
"""
|
if search.startswith('queue:'):
|
||||||
Apply a dict-based set of filters & parameters to a queryset.
|
return Q(queue__title__icontains=search[len('queue:'):])
|
||||||
|
if search.startswith('priority:'):
|
||||||
queryset is a Django queryset, eg MyModel.objects.all() or
|
return Q(priority__icontains=search[len('priority:'):])
|
||||||
MyModel.objects.filter(user=request.user)
|
filter = Q()
|
||||||
|
for subsearch in search.split("OR"):
|
||||||
params is a dictionary that contains the following:
|
subsearch = subsearch.strip()
|
||||||
filtering: A dict of Django ORM filters, eg:
|
filter = (
|
||||||
{'user__id__in': [1, 3, 103], 'title__contains': 'foo'}
|
filter |
|
||||||
|
Q(id__icontains=subsearch) |
|
||||||
search_string: A freetext search string
|
Q(title__icontains=subsearch) |
|
||||||
|
Q(description__icontains=subsearch) |
|
||||||
sorting: The name of the column to sort by
|
Q(priority__icontains=subsearch) |
|
||||||
"""
|
Q(resolution__icontains=subsearch) |
|
||||||
for key in params['filtering'].keys():
|
Q(submitter_email__icontains=subsearch) |
|
||||||
filter = {key: params['filtering'][key]}
|
Q(assigned_to__email__icontains=subsearch) |
|
||||||
queryset = queryset.filter(**filter)
|
Q(ticketcustomfieldvalue__value__icontains=subsearch) |
|
||||||
|
Q(created__icontains=subsearch) |
|
||||||
search = params.get('search_string', '')
|
Q(due_date__icontains=subsearch)
|
||||||
if search:
|
|
||||||
qset = (
|
|
||||||
Q(title__icontains=search) |
|
|
||||||
Q(description__icontains=search) |
|
|
||||||
Q(resolution__icontains=search) |
|
|
||||||
Q(submitter_email__icontains=search) |
|
|
||||||
Q(ticketcustomfieldvalue__value__icontains=search)
|
|
||||||
)
|
)
|
||||||
|
return filter
|
||||||
queryset = queryset.filter(qset)
|
|
||||||
|
|
||||||
sorting = params.get('sorting', None)
|
|
||||||
if sorting:
|
|
||||||
sortreverse = params.get('sortreverse', None)
|
|
||||||
if sortreverse:
|
|
||||||
sorting = "-%s" % sorting
|
|
||||||
queryset = queryset.order_by(sorting)
|
|
||||||
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
def get_query(query, huser):
|
DATATABLES_ORDER_COLUMN_CHOICES = Choices(
|
||||||
# Prefilter the allowed tickets
|
|
||||||
objects = cache.get(huser.user.email + query)
|
|
||||||
if objects is not None:
|
|
||||||
return objects
|
|
||||||
tickets = huser.get_tickets_in_queues().select_related()
|
|
||||||
query_params = query_from_base64(query)
|
|
||||||
ticket_qs = apply_query(tickets, query_params)
|
|
||||||
cache.set(huser.user.email + query, ticket_qs, timeout=3600)
|
|
||||||
return ticket_qs
|
|
||||||
|
|
||||||
|
|
||||||
ORDER_COLUMN_CHOICES = Choices(
|
|
||||||
('0', 'id'),
|
('0', 'id'),
|
||||||
('2', 'priority'),
|
('2', 'priority'),
|
||||||
('3', 'title'),
|
('3', 'title'),
|
||||||
@ -112,45 +87,135 @@ ORDER_COLUMN_CHOICES = Choices(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def query_tickets_by_args(objects, order_by, **kwargs):
|
def get_query_class():
|
||||||
"""
|
from django.conf import settings
|
||||||
This function takes in a list of ticket objects from the views and throws it
|
|
||||||
to the datatables on ticket_list.html. If a search string was entered, this
|
|
||||||
function filters existing dataset on search string and returns a filtered
|
|
||||||
filtered list. The `draw`, `length` etc parameters are for datatables to
|
|
||||||
display meta data on the table contents. The returning queryset is passed
|
|
||||||
to a Serializer called DatatablesTicketSerializer in serializers.py.
|
|
||||||
"""
|
|
||||||
draw = int(kwargs.get('draw', None)[0])
|
|
||||||
length = int(kwargs.get('length', None)[0])
|
|
||||||
start = int(kwargs.get('start', None)[0])
|
|
||||||
search_value = kwargs.get('search[value]', None)[0]
|
|
||||||
order_column = kwargs.get('order[0][column]', None)[0]
|
|
||||||
order = kwargs.get('order[0][dir]', None)[0]
|
|
||||||
|
|
||||||
order_column = ORDER_COLUMN_CHOICES[order_column]
|
def _get_query_class():
|
||||||
# django orm '-' -> desc
|
return __Query__
|
||||||
if order == 'desc':
|
return getattr(settings,
|
||||||
order_column = '-' + order_column
|
'HELPDESK_QUERY_CLASS',
|
||||||
|
_get_query_class)()
|
||||||
|
|
||||||
queryset = objects.all().order_by(order_by)
|
|
||||||
total = queryset.count()
|
|
||||||
|
|
||||||
if search_value:
|
class __Query__:
|
||||||
queryset = queryset.filter(Q(id__icontains=search_value) |
|
def __init__(self, huser, base64query=None, query_params=None):
|
||||||
Q(priority__icontains=search_value) |
|
self.huser = huser
|
||||||
Q(title__icontains=search_value) |
|
self.params = query_params if query_params else query_from_base64(base64query)
|
||||||
Q(queue__title__icontains=search_value) |
|
self.base64 = base64query if base64query else query_to_base64(query_params)
|
||||||
Q(status__icontains=search_value) |
|
self.result = None
|
||||||
Q(created__icontains=search_value) |
|
|
||||||
Q(due_date__icontains=search_value) |
|
|
||||||
Q(assigned_to__email__icontains=search_value))
|
|
||||||
|
|
||||||
count = queryset.count()
|
def get_search_filter_args(self):
|
||||||
queryset = queryset.order_by(order_column)[start:start + length]
|
search = self.params.get('search_string', '')
|
||||||
return {
|
return get_search_filter_args(search)
|
||||||
'items': queryset,
|
|
||||||
'count': count,
|
def __run__(self, queryset):
|
||||||
'total': total,
|
"""
|
||||||
'draw': draw
|
Apply a dict-based set of filters & parameters to a queryset.
|
||||||
}
|
|
||||||
|
queryset is a Django queryset, eg MyModel.objects.all() or
|
||||||
|
MyModel.objects.filter(user=request.user)
|
||||||
|
|
||||||
|
params is a dictionary that contains the following:
|
||||||
|
filtering: A dict of Django ORM filters, eg:
|
||||||
|
{'user__id__in': [1, 3, 103], 'title__contains': 'foo'}
|
||||||
|
|
||||||
|
search_string: A freetext search string
|
||||||
|
|
||||||
|
sorting: The name of the column to sort by
|
||||||
|
"""
|
||||||
|
for key in self.params.get('filtering', {}).keys():
|
||||||
|
filter = {key: self.params['filtering'][key]}
|
||||||
|
queryset = queryset.filter(**filter)
|
||||||
|
queryset = queryset.filter(self.get_search_filter_args())
|
||||||
|
sorting = self.params.get('sorting', None)
|
||||||
|
if sorting:
|
||||||
|
sortreverse = self.params.get('sortreverse', None)
|
||||||
|
if sortreverse:
|
||||||
|
sorting = "-%s" % sorting
|
||||||
|
queryset = queryset.order_by(sorting)
|
||||||
|
return queryset.distinct() # https://stackoverflow.com/questions/30487056/django-queryset-contains-duplicate-entries
|
||||||
|
|
||||||
|
def get_cache_key(self):
|
||||||
|
return str(self.huser.user.pk) + ":" + self.base64
|
||||||
|
|
||||||
|
def refresh_query(self):
|
||||||
|
tickets = self.huser.get_tickets_in_queues().select_related()
|
||||||
|
ticket_qs = self.__run__(tickets)
|
||||||
|
cache.set(self.get_cache_key(), ticket_qs, timeout=3600)
|
||||||
|
return ticket_qs
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
# Prefilter the allowed tickets
|
||||||
|
objects = cache.get(self.get_cache_key())
|
||||||
|
if objects is not None:
|
||||||
|
return objects
|
||||||
|
return self.refresh_query()
|
||||||
|
|
||||||
|
def get_datatables_context(self, **kwargs):
|
||||||
|
"""
|
||||||
|
This function takes in a list of ticket objects from the views and throws it
|
||||||
|
to the datatables on ticket_list.html. If a search string was entered, this
|
||||||
|
function filters existing dataset on search string and returns a filtered
|
||||||
|
filtered list. The `draw`, `length` etc parameters are for datatables to
|
||||||
|
display meta data on the table contents. The returning queryset is passed
|
||||||
|
to a Serializer called DatatablesTicketSerializer in serializers.py.
|
||||||
|
"""
|
||||||
|
objects = self.get()
|
||||||
|
order_by = '-date_created'
|
||||||
|
draw = int(kwargs.get('draw', None)[0])
|
||||||
|
length = int(kwargs.get('length', None)[0])
|
||||||
|
start = int(kwargs.get('start', None)[0])
|
||||||
|
search_value = kwargs.get('search[value]', None)[0]
|
||||||
|
order_column = kwargs.get('order[0][column]', None)[0]
|
||||||
|
order = kwargs.get('order[0][dir]', None)[0]
|
||||||
|
|
||||||
|
order_column = DATATABLES_ORDER_COLUMN_CHOICES[order_column]
|
||||||
|
# django orm '-' -> desc
|
||||||
|
if order == 'desc':
|
||||||
|
order_column = '-' + order_column
|
||||||
|
|
||||||
|
queryset = objects.all().order_by(order_by)
|
||||||
|
total = queryset.count()
|
||||||
|
|
||||||
|
if search_value:
|
||||||
|
queryset = queryset.filter(get_search_filter_args(search_value))
|
||||||
|
|
||||||
|
count = queryset.count()
|
||||||
|
queryset = queryset.order_by(order_column)[start:start + length]
|
||||||
|
return {
|
||||||
|
'data': DatatablesTicketSerializer(queryset, many=True).data,
|
||||||
|
'recordsFiltered': count,
|
||||||
|
'recordsTotal': total,
|
||||||
|
'draw': draw
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_timeline_context(self):
|
||||||
|
events = []
|
||||||
|
|
||||||
|
for ticket in self.get():
|
||||||
|
for followup in ticket.followup_set.all():
|
||||||
|
event = {
|
||||||
|
'start_date': self.mk_timeline_date(followup.date),
|
||||||
|
'text': {
|
||||||
|
'headline': ticket.title + ' - ' + followup.title,
|
||||||
|
'text': (followup.comment if followup.comment else _('No text')) + '<br/> <a href="%s" class="btn" role="button">%s</a>' %
|
||||||
|
(reverse('helpdesk:view', kwargs={'ticket_id': ticket.pk}), _("View ticket")),
|
||||||
|
},
|
||||||
|
'group': _('Messages'),
|
||||||
|
}
|
||||||
|
events.append(event)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'events': events,
|
||||||
|
}
|
||||||
|
|
||||||
|
def mk_timeline_date(self, date):
|
||||||
|
return {
|
||||||
|
'year': date.year,
|
||||||
|
'month': date.month,
|
||||||
|
'day': date.day,
|
||||||
|
'hour': date.hour,
|
||||||
|
'minute': date.minute,
|
||||||
|
'second': date.second,
|
||||||
|
'second': date.second,
|
||||||
|
}
|
||||||
|
@ -15,6 +15,7 @@ datatables for ticket_list.html. Called from staff.datatables_ticket_list.
|
|||||||
class DatatablesTicketSerializer(serializers.ModelSerializer):
|
class DatatablesTicketSerializer(serializers.ModelSerializer):
|
||||||
ticket = serializers.SerializerMethodField()
|
ticket = serializers.SerializerMethodField()
|
||||||
assigned_to = serializers.SerializerMethodField()
|
assigned_to = serializers.SerializerMethodField()
|
||||||
|
submitter = serializers.SerializerMethodField()
|
||||||
created = serializers.SerializerMethodField()
|
created = serializers.SerializerMethodField()
|
||||||
due_date = serializers.SerializerMethodField()
|
due_date = serializers.SerializerMethodField()
|
||||||
status = serializers.SerializerMethodField()
|
status = serializers.SerializerMethodField()
|
||||||
@ -26,7 +27,7 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
|
|||||||
model = Ticket
|
model = Ticket
|
||||||
# fields = '__all__'
|
# fields = '__all__'
|
||||||
fields = ('ticket', 'id', 'priority', 'title', 'queue', 'status',
|
fields = ('ticket', 'id', 'priority', 'title', 'queue', 'status',
|
||||||
'created', 'due_date', 'assigned_to', 'row_class',
|
'created', 'due_date', 'assigned_to', 'submitter', 'row_class',
|
||||||
'time_spent')
|
'time_spent')
|
||||||
|
|
||||||
def get_queue(self, obj):
|
def get_queue(self, obj):
|
||||||
@ -53,6 +54,9 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
|
|||||||
else:
|
else:
|
||||||
return ("None")
|
return ("None")
|
||||||
|
|
||||||
|
def get_submitter(self, obj):
|
||||||
|
return obj.submitter_email
|
||||||
|
|
||||||
def get_time_spent(self, obj):
|
def get_time_spent(self, obj):
|
||||||
return format_time_spent(obj.time_spent)
|
return format_time_spent(obj.time_spent)
|
||||||
|
|
||||||
|
74
helpdesk/templates/helpdesk/base-head.html
Normal file
74
helpdesk/templates/helpdesk/base-head.html
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load saved_queries %}
|
||||||
|
{% load load_helpdesk_settings %}
|
||||||
|
{% load static from staticfiles %}
|
||||||
|
{% with request|load_helpdesk_settings as helpdesk_settings %}
|
||||||
|
{% with user|saved_queries as user_saved_queries_ %}
|
||||||
|
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="author" content="">
|
||||||
|
|
||||||
|
<title>{% block helpdesk_title %}Helpdesk{% endblock %} :: {% trans "Powered by django-helpdesk" %}</title>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Bootstrap Core CSS -->
|
||||||
|
{% if helpdesk_settings.HELPDESK_USE_CDN %}
|
||||||
|
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
{% else %}
|
||||||
|
<link href="{% static 'helpdesk/vendor/bootstrap/css/bootstrap.min.css' %}" rel="stylesheet">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link href="{% static 'helpdesk/vendor/fontawesome-free/css/all.min.css' %}" rel="stylesheet" type="text/css">
|
||||||
|
|
||||||
|
<!-- DataTables CSS-->
|
||||||
|
<link href="{% static 'helpdesk/vendor/datatables/css/dataTables.bootstrap4.css' %}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- MetisMenu CSS -->
|
||||||
|
<link href="{% static 'helpdesk/vendor/metisMenu/metisMenu.min.css' %}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Morris Charts CSS -->
|
||||||
|
{% if helpdesk_settings.HELPDESK_USE_CDN %}
|
||||||
|
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css">
|
||||||
|
{% else %}
|
||||||
|
<link href="{% static 'helpdesk/vendor/morrisjs/morris.css' %}" rel="stylesheet">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<link href="{% static 'helpdesk/css/sb-admin.css' %}" rel="stylesheet">
|
||||||
|
<link rel='stylesheet' href='{% static "helpdesk/helpdesk-extend.css" %}' type='text/css' media="screen" >
|
||||||
|
|
||||||
|
{% if user.id %}
|
||||||
|
<!-- RSS -->
|
||||||
|
<link rel='alternate' href='{% url 'helpdesk:rss_user' user.get_username %}' type='application/rss+xml' title='{% trans "My Open Tickets" %}' />
|
||||||
|
<link rel='alternate' href='{% url 'helpdesk:rss_activity' %}' type='application/rss+xml' title='{% trans "All Recent Activity" %}' />
|
||||||
|
<link rel='alternate' href='{% url 'helpdesk:rss_unassigned' %}' type='application/rss+xml' title='{% trans "Unassigned Tickets" %}' />
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
/* hide google translate top bar */
|
||||||
|
.goog-te-banner-frame {display: none !important;}
|
||||||
|
.goog-te-balloon-frame {display: none !important;}
|
||||||
|
/* hide google translate tooltips (generated for every translated item) */
|
||||||
|
.goog-tooltip {display: none !important; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
/* header */
|
||||||
|
#dropdown li.headerlink { width: auto; float: left; text-align: center; }
|
||||||
|
|
||||||
|
/* query list */
|
||||||
|
#dropdown li.headerlink ul { display: none;
|
||||||
|
text-align: left;
|
||||||
|
position: absolute;
|
||||||
|
padding: 5px;
|
||||||
|
z-index: 2; }
|
||||||
|
/* query entries */
|
||||||
|
#dropdown li.headerlink:hover ul { display: block; width: auto; }
|
||||||
|
#dropdown li.headerlink:hover ul li { padding: 5px; margin: 1px; float: none; display: block; }
|
||||||
|
</style>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
@ -9,69 +9,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
|
||||||
<meta charset="utf-8">
|
{% include 'helpdesk/base-head.html' %}
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
||||||
<meta name="description" content="">
|
|
||||||
<meta name="author" content="">
|
|
||||||
|
|
||||||
<title>{% block helpdesk_title %}Helpdesk{% endblock %} :: {% trans "Powered by django-helpdesk" %}</title>
|
|
||||||
|
|
||||||
<!-- Bootstrap Core CSS -->
|
|
||||||
{% if helpdesk_settings.HELPDESK_USE_CDN %}
|
|
||||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
{% else %}
|
|
||||||
<link href="{% static 'helpdesk/vendor/bootstrap/css/bootstrap.min.css' %}" rel="stylesheet">
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Font Awesome -->
|
|
||||||
<link href="{% static 'helpdesk/vendor/fontawesome-free/css/all.min.css' %}" rel="stylesheet" type="text/css">
|
|
||||||
|
|
||||||
<!-- DataTables CSS-->
|
|
||||||
<link href="{% static 'helpdesk/vendor/datatables/css/dataTables.bootstrap4.css' %}" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- MetisMenu CSS -->
|
|
||||||
<link href="{% static 'helpdesk/vendor/metisMenu/metisMenu.min.css' %}" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Morris Charts CSS -->
|
|
||||||
{% if helpdesk_settings.HELPDESK_USE_CDN %}
|
|
||||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css">
|
|
||||||
{% else %}
|
|
||||||
<link href="{% static 'helpdesk/vendor/morrisjs/morris.css' %}" rel="stylesheet">
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Custom CSS -->
|
|
||||||
<link href="{% static 'helpdesk/css/sb-admin.css' %}" rel="stylesheet">
|
|
||||||
<link rel='stylesheet' href='{% static "helpdesk/helpdesk-extend.css" %}' type='text/css' media="screen" >
|
|
||||||
|
|
||||||
<!-- RSS -->
|
|
||||||
<link rel='alternate' href='{% url 'helpdesk:rss_user' user.get_username %}' type='application/rss+xml' title='{% trans "My Open Tickets" %}' />
|
|
||||||
<link rel='alternate' href='{% url 'helpdesk:rss_activity' %}' type='application/rss+xml' title='{% trans "All Recent Activity" %}' />
|
|
||||||
<link rel='alternate' href='{% url 'helpdesk:rss_unassigned' %}' type='application/rss+xml' title='{% trans "Unassigned Tickets" %}' />
|
|
||||||
|
|
||||||
<style type="text/css">
|
|
||||||
/* hide google translate top bar */
|
|
||||||
.goog-te-banner-frame {display: none !important;}
|
|
||||||
.goog-te-balloon-frame {display: none !important;}
|
|
||||||
/* hide google translate tooltips (generated for every translated item) */
|
|
||||||
.goog-tooltip {display: none !important; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style type="text/css">
|
|
||||||
/* header */
|
|
||||||
#dropdown li.headerlink { width: auto; float: left; text-align: center; }
|
|
||||||
|
|
||||||
/* query list */
|
|
||||||
#dropdown li.headerlink ul { display: none;
|
|
||||||
text-align: left;
|
|
||||||
position: absolute;
|
|
||||||
padding: 5px;
|
|
||||||
z-index: 2; }
|
|
||||||
/* query entries */
|
|
||||||
#dropdown li.headerlink:hover ul { display: block; width: auto; }
|
|
||||||
#dropdown li.headerlink:hover ul li { padding: 5px; margin: 1px; float: none; display: block; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
{% block helpdesk_head %}{% endblock %}
|
{% block helpdesk_head %}{% endblock %}
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
@ -79,10 +17,10 @@
|
|||||||
<body id="bg-dark">
|
<body id="bg-dark">
|
||||||
|
|
||||||
{% include "helpdesk/navigation-header.html" %}
|
{% include "helpdesk/navigation-header.html" %}
|
||||||
|
|
||||||
<div id="wrapper">
|
<div id="wrapper">
|
||||||
{% include "helpdesk/navigation-sidebar.html" %}
|
{% include "helpdesk/navigation-sidebar.html" %}
|
||||||
|
|
||||||
<div id="content-wrapper">
|
<div id="content-wrapper">
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
@ -105,35 +43,9 @@
|
|||||||
<!-- /#wrapper -->
|
<!-- /#wrapper -->
|
||||||
|
|
||||||
{% include "helpdesk/debug.html" %}
|
{% include "helpdesk/debug.html" %}
|
||||||
|
|
||||||
|
|
||||||
<!-- jQuery and Bootstrap Core -->
|
|
||||||
{% if helpdesk_settings.HELPDESK_USE_CDN %}
|
|
||||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
|
|
||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js"></script>
|
|
||||||
{% else %}
|
|
||||||
<script src="{% static 'helpdesk/vendor/jquery/jquery.min.js' %}"></script>
|
|
||||||
<script src="{% static 'helpdesk/vendor/bootstrap/js/bootstrap.bundle.min.js' %}"></script>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Core plugin JavaScript-->
|
|
||||||
<script src="{% static 'helpdesk/vendor/jquery-easing/jquery.easing.min.js' %}"></script>
|
|
||||||
|
|
||||||
<!-- Page level plugin JavaScript-->
|
|
||||||
<script src="{% static 'helpdesk/vendor/chart.js/Chart.min.js' %}"></script>
|
|
||||||
<script src="{% static 'helpdesk/vendor/datatables/js/jquery.dataTables.js' %}"></script>
|
|
||||||
<script src="{% static 'helpdesk/vendor/datatables/js/dataTables.bootstrap4.js' %}"></script>
|
|
||||||
|
|
||||||
<!-- jQuery UI DatePicker -->
|
|
||||||
<script src='{% static "helpdesk/vendor/jquery-ui/jquery-ui.min.js" %}' type='text/javascript' language='javascript'></script>
|
|
||||||
<link href="{% static 'helpdesk/vendor/jquery-ui/jquery-ui.css' %}" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Metis Menu Plugin JavaScript -->
|
{% include 'helpdesk/base_js.html' %}
|
||||||
<script src="{% static 'helpdesk/vendor/metisMenu/metisMenu.min.js' %}"></script>
|
|
||||||
|
|
||||||
<!-- Custom Theme JavaScript -->
|
|
||||||
<script src="{% static 'helpdesk/js/sb-admin.js' %}"></script>
|
|
||||||
|
|
||||||
{% block helpdesk_js %}{% endblock %}
|
{% block helpdesk_js %}{% endblock %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
27
helpdesk/templates/helpdesk/base_js.html
Normal file
27
helpdesk/templates/helpdesk/base_js.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{% load static from staticfiles %}
|
||||||
|
<!-- jQuery and Bootstrap Core -->
|
||||||
|
{% if helpdesk_settings.HELPDESK_USE_CDN %}
|
||||||
|
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js"></script>
|
||||||
|
{% else %}
|
||||||
|
<script src="{% static 'helpdesk/vendor/jquery/jquery.min.js' %}"></script>
|
||||||
|
<script src="{% static 'helpdesk/vendor/bootstrap/js/bootstrap.bundle.min.js' %}"></script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Core plugin JavaScript-->
|
||||||
|
<script src="{% static 'helpdesk/vendor/jquery-easing/jquery.easing.min.js' %}"></script>
|
||||||
|
|
||||||
|
<!-- Page level plugin JavaScript-->
|
||||||
|
<script src="{% static 'helpdesk/vendor/chart.js/Chart.min.js' %}"></script>
|
||||||
|
<script src="{% static 'helpdesk/vendor/datatables/js/jquery.dataTables.js' %}"></script>
|
||||||
|
<script src="{% static 'helpdesk/vendor/datatables/js/dataTables.bootstrap4.js' %}"></script>
|
||||||
|
|
||||||
|
<!-- jQuery UI DatePicker -->
|
||||||
|
<script src='{% static "helpdesk/vendor/jquery-ui/jquery-ui.min.js" %}' type='text/javascript' language='javascript'></script>
|
||||||
|
<link href="{% static 'helpdesk/vendor/jquery-ui/jquery-ui.css' %}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Metis Menu Plugin JavaScript -->
|
||||||
|
<script src="{% static 'helpdesk/vendor/metisMenu/metisMenu.min.js' %}"></script>
|
||||||
|
|
||||||
|
<!-- Custom Theme JavaScript -->
|
||||||
|
<script src="{% static 'helpdesk/js/sb-admin.js' %}"></script>
|
15
helpdesk/templates/helpdesk/filters/kbitems.html
Normal file
15
helpdesk/templates/helpdesk/filters/kbitems.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{% load i18n humanize %}
|
||||||
|
{% load static from staticfiles %}
|
||||||
|
{% load in_list %}
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="col col-sm-3">
|
||||||
|
<label for='id_statuses'>{% trans "Knowledge base item(s)" %}:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col col-sm-3">
|
||||||
|
<select id='id_kbitems' name='kbitem' multiple='selected' size='5'>{% for s in kbitem_choices %}<option value='{{ s.0 }}'{% if s.0|in_list:query_params.filtering.kbitem__in %} selected='selected'{% endif %}>{{ s.1 }}</option>{% endfor %}</select>
|
||||||
|
</div>
|
||||||
|
<div class="col col-sm-6">
|
||||||
|
<button class="filterBuilderRemove btn btn-danger btn-sm float-right"><i class="fas fa-trash-alt"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class='form-row filterHelp'>{% trans "Ctrl-click to select multiple options" %}</div>
|
||||||
|
</div>
|
@ -9,5 +9,5 @@
|
|||||||
<div class="col col-sm-6">
|
<div class="col col-sm-6">
|
||||||
<button class='filterBuilderRemove btn btn-danger btn-sm float-right'><i class="fas fa-trash-alt"></i></button>
|
<button class='filterBuilderRemove btn btn-danger btn-sm float-right'><i class="fas fa-trash-alt"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<p class='filterHelp'>{% trans "Keywords are case-insensitive, and will be looked for in the title, body and submitter fields." %}</p>
|
<p class='filterHelp'>{% trans "Keywords are case-insensitive, and will be looked for pretty much everywhere possible. Prepend with 'queue:' or 'priority:' to search by queue or priority. You can also use the keyword OR to combine multiple searches." %}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
{% extends "helpdesk/public_base.html" %}{% load i18n humanize %}
|
{% extends "helpdesk/public_base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block helpdesk_title %}{% blocktrans with category.title as kbcat %}{{ kbcat }}{% endblocktrans %}{% endblock %}
|
||||||
|
|
||||||
{% block helpdesk_breadcrumb %}
|
{% block helpdesk_breadcrumb %}
|
||||||
<li class="breadcrumb-item">
|
<li class="breadcrumb-item">
|
||||||
@ -8,38 +11,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block helpdesk_body %}
|
{% block helpdesk_body %}
|
||||||
<h2>{% trans 'Knowledgebase Category' %}:{% blocktrans with category.title as kbcat %}{{ kbcat }}{% endblocktrans %}</h2>
|
{% include 'helpdesk/kb_category_base.html' %}
|
||||||
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">
|
|
||||||
<i class="fas fa-info-circle"></i>
|
|
||||||
{% blocktrans with category.title as kbcat %}You are viewing all items in the {{ kbcat }} category.{% endblocktrans %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p>{{ category.description }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% for item in items %}
|
|
||||||
{% cycle 'one' 'two' 'three' as itemnumperrow silent %}
|
|
||||||
{% ifequal itemnumperrow 'one' %}<div class="card-deck">{% endifequal %}
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title">{{ item.title }}</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="card-text">{{ item.question }}</p>
|
|
||||||
<p class="card-text">
|
|
||||||
{% blocktrans with item.get_absolute_url as url %}<a href='{{ url }}' class="btn btn-primary"> Go to answer <i class="fa fa-share"></i></a>{% endblocktrans %}
|
|
||||||
</p>
|
|
||||||
<div class="well well-sm">
|
|
||||||
<p>{% trans 'Rating' %}: {{ item.score }}</p>
|
|
||||||
<p>{% trans 'Last Update' %}: {{ item.last_updated|naturaltime }}</p>
|
|
||||||
</div>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% ifequal itemnumperrow 'three' %}</div>{% endifequal %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
48
helpdesk/templates/helpdesk/kb_category_base.html
Normal file
48
helpdesk/templates/helpdesk/kb_category_base.html
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
<h2>{% trans 'Knowledgebase Category' %}:{% blocktrans with category.title as kbcat %}{{ kbcat }}{% endblocktrans %}</h2>
|
||||||
|
<p>{{ category.description }}</p>
|
||||||
|
<div id="accordion">
|
||||||
|
{% for item in items %}
|
||||||
|
<div class="card mb-3">
|
||||||
|
<button class="btn btn-link" data-toggle="collapse" data-target="#collapse{{item.id}}" aria-expanded="true" aria-controls="collapse{{item.id}}">
|
||||||
|
<div class="card-header" id="header{{item.id}}">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
{{ item.title }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div id="collapse{{item.id}}" class="collapse {% if item.id == selected_item %}show{% endif %}" aria-labelledby="header{{item.id}}" data-parent="#accordion">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-text">{{ item.question }}</p>
|
||||||
|
<p>{{ item.get_markdown }}</p>
|
||||||
|
<div class="row">
|
||||||
|
{% if request.user.pk %}
|
||||||
|
<div class="col-sm">
|
||||||
|
<a href='{% url "helpdesk:kb_vote" item.pk %}?vote=up'><button type="button" class="btn btn-success btn-circle btn-xl"><i class="fa fa-thumbs-up fa-lg"></i></button></a>
|
||||||
|
<a href='{% url "helpdesk:kb_vote" item.pk %}?vote=down'><button type="button" class="btn btn-danger btn-circle btn-xl"><i class="fa fa-thumbs-down fa-lg"></i></button></a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if staff %}
|
||||||
|
<a href='{% url 'helpdesk:list' %}?kbitem={{item.id}}' class="col-sm">
|
||||||
|
<button type="button" class="btn btn-success btn-circle btn-xl float-right"><i class="fa fa-search fa-lg"></i> {{item.num_open_tickets}} {% trans 'open tickets' %}</button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
<a href='{% if iframe %}{% url 'helpdesk:submit_iframe' %}{% else %}{% url 'helpdesk:submit' %}{%endif%}?{% if category.queue %}queue={{category.queue.pk}};_readonly_fields_=queue;{%endif%}kbitem={{item.id}};{{query_param_string}}' class="col-sm">
|
||||||
|
<button type="button" class="btn btn-success btn-circle btn-xl float-right"><i class="fa fa-envelope fa-lg"></i> {% trans 'Contact a human' %}</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{% blocktrans with recommendations=item.recommendations votes=item.votes %}{{ recommendations }} people found this answer useful of {{votes}}{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if category.queue %}
|
||||||
|
<a href='{% if iframe %}{% url 'helpdesk:submit_iframe' %}{% else %}{% url 'helpdesk:submit' %}{%endif%}?queue={{category.queue.pk}};_readonly_fields_=queue;{{query_param_string}}'>
|
||||||
|
<button type="button" class="btn btn-danger btn-circle btn-xl float-right"><i class="fa fa-envelope fa-lg"></i> {% trans 'Contact a human' %}</button>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
13
helpdesk/templates/helpdesk/kb_category_iframe.html
Normal file
13
helpdesk/templates/helpdesk/kb_category_iframe.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load saved_queries %}
|
||||||
|
<head>
|
||||||
|
{% include 'helpdesk/base-head.html' %}
|
||||||
|
{% block helpdesk_head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{% block helpdesk_body %}
|
||||||
|
{% include 'helpdesk/kb_category_base.html' %}
|
||||||
|
{% endblock %}
|
||||||
|
{% include 'helpdesk/base_js.html' %}
|
||||||
|
</body>
|
@ -1,51 +0,0 @@
|
|||||||
{% extends "helpdesk/public_base.html" %}{% load i18n %}
|
|
||||||
|
|
||||||
{% block helpdesk_breadcrumb %}
|
|
||||||
<li class="breadcrumb-item">
|
|
||||||
<a href="{% url 'helpdesk:kb_index' %}">{% trans "Knowledgebase" %}</a>
|
|
||||||
</li>
|
|
||||||
<li class="breadcrumb-item">
|
|
||||||
<a href="{{ category.get_absolute_url }}">{% blocktrans with category.title as kbcat %}{{ kbcat }}{% endblocktrans %}</a>
|
|
||||||
</li>
|
|
||||||
<li class="breadcrumb-item active">Overview</li>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block helpdesk_body %}
|
|
||||||
<h2>{% trans 'Knowledgebase' %}: {% blocktrans with item.title as item %}{{ item }}{% endblocktrans %}</h2>
|
|
||||||
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">
|
|
||||||
<i class="fas fa-question-circle"></i>
|
|
||||||
{{ item.question }}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p>{{ item.get_markdown }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-2">
|
|
||||||
<p>{% trans "Did you find this article useful?" %}</p>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<a href='vote/?vote=up'><button type="button" class="btn btn-success btn-circle btn-xl"><i class="fa fa-thumbs-up fa-lg"></i></button></a>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<a href='vote/?vote=down'><button type="button" class="btn btn-danger btn-circle btn-xl"><i class="fa fa-thumbs-down fa-lg"></i></button></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-10">
|
|
||||||
<p>{% trans "The results of voting by other readers of this article are below:" %}</p>
|
|
||||||
<ul>
|
|
||||||
<li>{% blocktrans with item.recommendations as recommendations %}Recommendations: {{ recommendations }}{% endblocktrans %}</li>
|
|
||||||
<li>{% blocktrans with item.votes as votes %}Votes: {{ votes }}{% endblocktrans %}</li>
|
|
||||||
<li>{% blocktrans with item.score as score %}Overall Rating: {{ score }}{% endblocktrans %}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>{% blocktrans with item.category.title as category_title and item.category.get_absolute_url as category_url %}View <a href='{{ category_url }}'>other <em>{{ category_title }}</em> articles</a>, or continue <a href='../'>viewing other knowledgebase articles</a>.{% endblocktrans %}</p>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -1,47 +1,13 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load load_helpdesk_settings %}
|
|
||||||
{% load static from staticfiles %}
|
{% load static from staticfiles %}
|
||||||
|
{% load load_helpdesk_settings %}
|
||||||
{% with request|load_helpdesk_settings as helpdesk_settings %}
|
{% with request|load_helpdesk_settings as helpdesk_settings %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
|
||||||
<meta charset="utf-8">
|
{% include 'helpdesk/base-head.html' %}
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
||||||
<meta name="description" content="">
|
|
||||||
<meta name="author" content="">
|
|
||||||
|
|
||||||
<title>{% block helpdesk_title %}{% trans 'Helpdesk' %}{% endblock %} :: {% trans "Powered by django-helpdesk" %}</title>
|
|
||||||
|
|
||||||
<!-- Bootstrap Core CSS -->
|
|
||||||
{% if helpdesk_settings.HELPDESK_USE_CDN %}
|
|
||||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
{% else %}
|
|
||||||
<link href="{% static 'helpdesk/vendor/bootstrap/css/bootstrap.min.css' %}" rel="stylesheet">
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Font Awesome -->
|
|
||||||
<link href="{% static 'helpdesk/vendor/fontawesome-free/css/all.min.css' %}" rel="stylesheet" type="text/css">
|
|
||||||
|
|
||||||
<!-- DataTables CSS-->
|
|
||||||
<link href="{% static 'helpdesk/vendor/datatables/css/dataTables.bootstrap4.css' %}" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- MetisMenu CSS -->
|
|
||||||
<link href="{% static 'helpdesk/vendor/metisMenu/metisMenu.min.css' %}" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Custom CSS -->
|
|
||||||
<link href="{% static 'helpdesk/css/sb-admin.css' %}" rel="stylesheet">
|
|
||||||
<link rel='stylesheet' href='{% static "helpdesk/helpdesk-extend.css" %}' type='text/css' media="screen" >
|
|
||||||
|
|
||||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
|
||||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
|
||||||
<!--[if lt IE 9]>
|
|
||||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
|
||||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
|
|
||||||
<![endif]-->
|
|
||||||
|
|
||||||
{% block helpdesk_head %}{% endblock %}
|
{% block helpdesk_head %}{% endblock %}
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
@ -49,62 +15,35 @@
|
|||||||
<body id="bg-dark">
|
<body id="bg-dark">
|
||||||
|
|
||||||
{% include "helpdesk/navigation-header.html" %}
|
{% include "helpdesk/navigation-header.html" %}
|
||||||
|
|
||||||
<div id="wrapper">
|
<div id="wrapper">
|
||||||
{% include "helpdesk/navigation-sidebar.html" %}
|
{% include "helpdesk/navigation-sidebar.html" %}
|
||||||
|
|
||||||
<div id="content-wrapper">
|
<div id="content-wrapper">
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
|
||||||
<!-- Breadcrumbs-->
|
<!-- Breadcrumbs-->
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
{% block helpdesk_breadcrumb %}{% endblock %}
|
{% block helpdesk_breadcrumb %}{% endblock %}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
{% block helpdesk_body %}{% endblock %}
|
{% block helpdesk_body %}{% endblock %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- /.container-fluid -->
|
<!-- /.container-fluid -->
|
||||||
|
|
||||||
{% include "helpdesk/attribution.html" %}
|
{% include "helpdesk/attribution.html" %}
|
||||||
</div>
|
</div>
|
||||||
<!-- /.content-wrapper -->
|
<!-- /.content-wrapper -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- /#wrapper -->
|
<!-- /#wrapper -->
|
||||||
|
|
||||||
{% include "helpdesk/debug.html" %}
|
{% include "helpdesk/debug.html" %}
|
||||||
|
|
||||||
<!-- jQuery and Bootstrap Core -->
|
{% include 'helpdesk/base_js.html' %}
|
||||||
{% if helpdesk_settings.HELPDESK_USE_CDN %}
|
|
||||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
|
|
||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js"></script>
|
|
||||||
{% else %}
|
|
||||||
<script src="{% static 'helpdesk/vendor/jquery/jquery.min.js' %}"></script>
|
|
||||||
<script src="{% static 'helpdesk/vendor/bootstrap/js/bootstrap.bundle.min.js' %}"></script>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Core plugin JavaScript-->
|
|
||||||
<script src="{% static 'helpdesk/vendor/jquery-easing/jquery.easing.min.js' %}"></script>
|
|
||||||
|
|
||||||
<!-- Page level plugin JavaScript-->
|
|
||||||
<script src="{% static 'helpdesk/vendor/chart.js/Chart.min.js' %}"></script>
|
|
||||||
<script src="{% static 'helpdesk/vendor/datatables/js/jquery.dataTables.js' %}"></script>
|
|
||||||
<script src="{% static 'helpdesk/vendor/datatables/js/dataTables.bootstrap4.js' %}"></script>
|
|
||||||
|
|
||||||
<!-- jQuery UI DatePicker -->
|
|
||||||
<script src='{% static "helpdesk/vendor/jquery-ui/jquery-ui.min.js" %}' type='text/javascript' language='javascript'></script>
|
|
||||||
<link href="{% static 'helpdesk/vendor/jquery-ui/jquery-ui.css' %}" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Metis Menu Plugin JavaScript -->
|
|
||||||
<script src="{% static 'helpdesk/vendor/metisMenu/metisMenu.min.js' %}"></script>
|
|
||||||
|
|
||||||
<!-- Custom Theme JavaScript -->
|
|
||||||
<script src="{% static 'helpdesk/js/sb-admin.js' %}"></script>
|
|
||||||
|
|
||||||
{% block helpdesk_js %}{% endblock %}
|
{% block helpdesk_js %}{% endblock %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% extends "helpdesk/public_base.html" %}
|
{% extends "helpdesk/public_base.html" %}
|
||||||
{% load i18n bootstrap4form %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block helpdesk_title %}{% trans "Create Ticket" %}{% endblock %}
|
{% block helpdesk_title %}{% trans "Create Ticket" %}{% endblock %}
|
||||||
|
|
||||||
@ -11,20 +11,13 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block helpdesk_body %}
|
{% block helpdesk_body %}
|
||||||
{% if helpdesk_settings.HELPDESK_SUBMIT_A_TICKET_PUBLIC %}
|
<div class="container">
|
||||||
<div class="container">
|
<div class="card card-register mx-auto mt-5">
|
||||||
<div class="card card-register mx-auto mt-5">
|
|
||||||
<div class="card-header">{% trans "Submit a Ticket" %}</div>
|
<div class="card-header">{% trans "Submit a Ticket" %}</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>{% trans "Unless otherwise stated, all fields are required." %} {% trans "Please provide as descriptive a title and description as possible." %}</p>
|
{% include 'helpdesk/public_create_ticket_base.html' %}
|
||||||
<form method='post' action='./#submit' enctype='multipart/form-data'>
|
|
||||||
{{ form|bootstrap4form }}
|
|
||||||
<button type="submit" class="btn btn-primary btn-lg btn-block"><i class="fa fa-send"></i> {% trans "Submit Ticket" %}</button>
|
|
||||||
{% csrf_token %}</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
</div>
|
||||||
<h2>{% trans "Public ticket submission is disabled. Please contact the administrator for assistance." %}</h2>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
14
helpdesk/templates/helpdesk/public_create_ticket_base.html
Normal file
14
helpdesk/templates/helpdesk/public_create_ticket_base.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% load i18n bootstrap4form %}
|
||||||
|
{% load load_helpdesk_settings %}
|
||||||
|
{% with request|load_helpdesk_settings as helpdesk_settings %}
|
||||||
|
|
||||||
|
{% if helpdesk_settings.HELPDESK_SUBMIT_A_TICKET_PUBLIC %}
|
||||||
|
<p>{% trans "Unless otherwise stated, all fields are required." %} {% trans "Please provide as descriptive a title and description as possible." %}</p>
|
||||||
|
<form method='post' enctype='multipart/form-data'>
|
||||||
|
{{ form|bootstrap4form }}
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg btn-block"><i class="fa fa-send"></i> {% trans "Submit Ticket" %}</button>
|
||||||
|
{% csrf_token %}</form>
|
||||||
|
{% else %}
|
||||||
|
<h2>{% trans "Public ticket submission is disabled. Please contact the administrator for assistance." %}</h2>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
11
helpdesk/templates/helpdesk/public_create_ticket_iframe.html
Normal file
11
helpdesk/templates/helpdesk/public_create_ticket_iframe.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load saved_queries %}
|
||||||
|
<head>
|
||||||
|
{% include 'helpdesk/base-head.html' %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{% block helpdesk_body %}
|
||||||
|
{% include 'helpdesk/public_create_ticket_base.html' %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
4
helpdesk/templates/helpdesk/success_iframe.html
Normal file
4
helpdesk/templates/helpdesk/success_iframe.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
<h1>
|
||||||
|
{% trans "Ticket submitted successfully! We will reply via email as soon as we get the chance." %}
|
||||||
|
</h1>
|
@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
<!-- Tab panes -->
|
<!-- Tab panes -->
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="tab-pane fade in active" id="EmailCC">
|
<div class="tab-pane in active" id="EmailCC">
|
||||||
<h4>{% trans 'Add Email' %}</h4>
|
<h4>{% trans 'Add Email' %}</h4>
|
||||||
<form method='post' action='./'>
|
<form method='post' action='./'>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
@ -37,8 +37,10 @@
|
|||||||
</strong>{% endifequal %}
|
</strong>{% endifequal %}
|
||||||
</td>
|
</td>
|
||||||
<th class="table-active">{% trans "Submitter E-Mail" %}</th>
|
<th class="table-active">{% trans "Submitter E-Mail" %}</th>
|
||||||
<td>{{ ticket.submitter_email }}
|
<td> {{ ticket.submitter_email }}
|
||||||
{% if user.is_superuser %} {% if submitter_userprofile_url %}<strong><a href='{{submitter_userprofile_url}}'><button type="button" class="btn btn-primary btn-sm"><i class="fas fa-address-book"></i></button></a></strong>{% endif %}
|
{% if user.is_superuser %} {% if submitter_userprofile_url %}<strong><a href='{{submitter_userprofile_url}}'><button type="button" class="btn btn-primary btn-sm"><i class="fas fa-address-book"></i></button></a></strong>{% endif %}
|
||||||
|
<strong><a href ="{% url 'helpdesk:list'%}?q={{ticket.submitter_email}}">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm"><i class="fas fa-search"></i></button></a></strong>
|
||||||
<strong><a href='{% url 'helpdesk:email_ignore_add' %}?email={{ ticket.submitter_email }}'>
|
<strong><a href='{% url 'helpdesk:email_ignore_add' %}?email={{ ticket.submitter_email }}'>
|
||||||
<button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-eye-slash"></i></button></a></strong>{% endif %}
|
<button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-eye-slash"></i></button></a></strong>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
@ -66,6 +68,12 @@
|
|||||||
<th class="table-active">{% trans "Total time spent" %}</th>
|
<th class="table-active">{% trans "Total time spent" %}</th>
|
||||||
<td>{{ ticket.time_spent_formated }}</td>
|
<td>{{ ticket.time_spent_formated }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if ticket.kbitem %}
|
||||||
|
<tr>
|
||||||
|
<th class="table-active">{% trans "Knowlegebase item" %}</th>
|
||||||
|
<td> <a href ="{{ticket.kbitem.query_url}}"> {{ticket.kbitem.title}} </a> </td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="table-active">{% trans "Attachments" %}</th>
|
<th class="table-active">{% trans "Attachments" %}</th>
|
||||||
<td colspan="3">
|
<td colspan="3">
|
||||||
|
@ -30,6 +30,116 @@
|
|||||||
{% block helpdesk_body %}
|
{% block helpdesk_body %}
|
||||||
{% load in_list %}
|
{% load in_list %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li class="nav-item" style="width: 200px;">
|
||||||
|
{% trans "Query Results" %}:
|
||||||
|
</li>
|
||||||
|
<li class="nav-item"">
|
||||||
|
<a class="nav-link active" href="#datatabletabcontents" id="datatabletabcontents-tab" data-toggle="tab" role="tab" aria-controls="datatabletabcontents" aria-selected=true>
|
||||||
|
<i class="fas fa-th-list"></i>
|
||||||
|
{% trans "Table" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#timelinetabcontents" id="timelinetabcontents-tab" data-toggle="tab" role="tab" aria-controls="timelinetabcontents" aria-selected=false>
|
||||||
|
<i class="fas fa-history"></i>
|
||||||
|
{% trans "Timeline" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{{ search_message|safe }}
|
||||||
|
<div class="tab-content" id="myTabContent">
|
||||||
|
<div class="tab-pane fade show active" id="datatabletabcontents" role="tabpanel" aria-labelledby="datatabletabcontents-tab">
|
||||||
|
<form method='post' action='{% url 'helpdesk:mass_update' %}' id="ticket_mass_update">
|
||||||
|
<table width="100%" class="table table-sm table-striped table-bordered table-hover" id="ticketTable" data-page-length='{{ default_tickets_per_page }}'>
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th> </th>
|
||||||
|
<th>{% trans "Ticket" %}</th>
|
||||||
|
<th>{% trans "Prority" %}</th>
|
||||||
|
<th>{% trans "Queue" %}</th>
|
||||||
|
<th>{% trans "Status" %}</th>
|
||||||
|
<th>{% trans "Created" %}</th>
|
||||||
|
<th>{% trans "Due Date" %}</th>
|
||||||
|
<th>{% trans "Owner" %}</th>
|
||||||
|
<th>{% trans "Submitter" %}</th>
|
||||||
|
<th>{% trans "Time Spent" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p><label>{% trans "Select:" %} </label>
|
||||||
|
|
||||||
|
<button id="select_all_btn" type="button" class="btn btn-primary btn-sm" />
|
||||||
|
<i class="fas fa-check-circle"></i> {% trans "All" %}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id='select_none_btn' type="button" class="btn btn-primary btn-sm"><i class="fas fa-times-circle"></i> {% trans "None" %}</button>
|
||||||
|
|
||||||
|
<button id='select_inverse_btn' type="button" class="btn btn-primary btn-sm"><i class="fas fa-expand-arrows-alt"></i> {% trans "Invert" %}</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for='id_mass_action'>{% trans "With Selected Tickets:" %}</label>
|
||||||
|
<select name='action' id='id_mass_action'>
|
||||||
|
<option value='take'>{% trans "Take (Assign to me)" %}</option>
|
||||||
|
<option value='delete'>{% trans "Delete" %}</option>
|
||||||
|
<optgroup label='{% trans "Close" %}'>
|
||||||
|
<option value='close'>{% trans "Close (Don't Send E-Mail)" %}</option>
|
||||||
|
<option value='close_public'>{% trans "Close (Send E-Mail)" %}</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label='{% trans "Assign To" %}'>
|
||||||
|
<option value='unassign'>{% trans "Nobody (Unassign)" %}</option>
|
||||||
|
{% for u in user_choices %}<option value='assign_{{ u.id }}'>{{ u.get_username }}</option>{% endfor %}
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm"><i class="fas fa-arrow-circle-right"></i> {% trans "Go" %}</button>
|
||||||
|
</p>
|
||||||
|
{% csrf_token %}</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="timelinetabcontents" role="tabpanel" aria-labelledby="timelinetabcontents-tab">
|
||||||
|
<div id='timeline-embed' style="width: 100%; height: 80vh"></div>
|
||||||
|
<!-- 1 -->
|
||||||
|
<link title="timeline-styles" rel="stylesheet" href="https://cdn.knightlab.com/libs/timeline3/latest/css/timeline.css">
|
||||||
|
|
||||||
|
<!-- 2 -->
|
||||||
|
<script src="https://cdn.knightlab.com/libs/timeline3/latest/js/timeline.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 3 -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
// The TL.Timeline constructor takes at least two arguments:
|
||||||
|
// the id of the Timeline container (no '#'), and
|
||||||
|
// the URL to your JSON data file or Google spreadsheet.
|
||||||
|
// the id must refer to an element "above" this code,
|
||||||
|
// and the element must have CSS styling to give it width and height
|
||||||
|
// optionally, a third argument with configuration options can be passed.
|
||||||
|
// See below for more about options.
|
||||||
|
var timeline_loaded = false;
|
||||||
|
$(function () {
|
||||||
|
$('#timelinetabcontents-tab').on('shown.bs.tab', function (e) {
|
||||||
|
if(!timeline_loaded){
|
||||||
|
timeline = new TL.Timeline(
|
||||||
|
'timeline-embed',
|
||||||
|
'{% url 'helpdesk:timeline_ticket_list' urlsafe_query %}'
|
||||||
|
);
|
||||||
|
timeline_loaded = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- /.panel-body -->
|
||||||
|
</div>
|
||||||
|
<!-- /.panel -->
|
||||||
|
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="fas fa-hand-pointer"></i>
|
<i class="fas fa-hand-pointer"></i>
|
||||||
@ -62,6 +172,7 @@
|
|||||||
<option id="filterBuilderSelect-Status" value="Status">{% trans "Status" %}</option>
|
<option id="filterBuilderSelect-Status" value="Status">{% trans "Status" %}</option>
|
||||||
<option id="filterBuilderSelect-Keywords" value="Keywords">{% trans "Keywords" %}</option>
|
<option id="filterBuilderSelect-Keywords" value="Keywords">{% trans "Keywords" %}</option>
|
||||||
<option id="filterBuilderSelect-Dates" value="Dates">{% trans "Date Range" %}</option>
|
<option id="filterBuilderSelect-Dates" value="Dates">{% trans "Date Range" %}</option>
|
||||||
|
<option id="filterBuilderSelect-KBItems" value="KBItems">{% trans "Knowledge base items" %}</option>
|
||||||
</select>
|
</select>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
</form>
|
</form>
|
||||||
@ -87,6 +198,9 @@
|
|||||||
<li class="list-group-item filterBox{% if query_params.search_string %} filterBoxShow{% endif %}" id="filterBoxKeywords">
|
<li class="list-group-item filterBox{% if query_params.search_string %} filterBoxShow{% endif %}" id="filterBoxKeywords">
|
||||||
{% include './filters/keywords.html' %}
|
{% include './filters/keywords.html' %}
|
||||||
</li>
|
</li>
|
||||||
|
<li class="list-group-item filterBox{% if query_params.filtering.kbitem__in %} filterBoxShow{% endif %}" id="filterBoxKBItems">
|
||||||
|
{% include './filters/kbitems.html' %}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<input class="btn btn-primary btn-sm" type='submit' value='{% trans "Apply Filters" %}' />
|
<input class="btn btn-primary btn-sm" type='submit' value='{% trans "Apply Filters" %}' />
|
||||||
@ -162,64 +276,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- end top card -->
|
<!-- end top card -->
|
||||||
|
|
||||||
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">
|
|
||||||
<i class="fas fa-table"></i>
|
|
||||||
{% trans "Query Results" %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{{ search_message|safe }}
|
|
||||||
<form method='post' action='{% url 'helpdesk:mass_update' %}' id="ticket_mass_update">
|
|
||||||
<table width="100%" class="table table-sm table-striped table-bordered table-hover" id="ticketTable" data-page-length='{{ default_tickets_per_page }}'>
|
|
||||||
<thead class="thead-light">
|
|
||||||
<tr>
|
|
||||||
<th> </th>
|
|
||||||
<th>{% trans "Ticket" %}</th>
|
|
||||||
<th>{% trans "Prority" %}</th>
|
|
||||||
<th>{% trans "Queue" %}</th>
|
|
||||||
<th>{% trans "Status" %}</th>
|
|
||||||
<th>{% trans "Created" %}</th>
|
|
||||||
<th>{% trans "Due Date" %}</th>
|
|
||||||
<th>{% trans "Owner" %}</th>
|
|
||||||
<th>{% trans "Time Spent" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p><label>{% trans "Select:" %} </label>
|
|
||||||
|
|
||||||
<button id="select_all_btn" type="button" class="btn btn-primary btn-sm" />
|
|
||||||
<i class="fas fa-check-circle"></i> {% trans "All" %}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button id='select_none_btn' type="button" class="btn btn-primary btn-sm"><i class="fas fa-times-circle"></i> {% trans "None" %}</button>
|
|
||||||
|
|
||||||
<button id='select_inverse_btn' type="button" class="btn btn-primary btn-sm"><i class="fas fa-expand-arrows-alt"></i> {% trans "Invert" %}</button>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<label for='id_mass_action'>{% trans "With Selected Tickets:" %}</label>
|
|
||||||
<select name='action' id='id_mass_action'>
|
|
||||||
<option value='take'>{% trans "Take (Assign to me)" %}</option>
|
|
||||||
<option value='delete'>{% trans "Delete" %}</option>
|
|
||||||
<optgroup label='{% trans "Close" %}'>
|
|
||||||
<option value='close'>{% trans "Close (Don't Send E-Mail)" %}</option>
|
|
||||||
<option value='close_public'>{% trans "Close (Send E-Mail)" %}</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label='{% trans "Assign To" %}'>
|
|
||||||
<option value='unassign'>{% trans "Nobody (Unassign)" %}</option>
|
|
||||||
{% for u in user_choices %}<option value='assign_{{ u.id }}'>{{ u.get_username }}</option>{% endfor %}
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
<button type="submit" class="btn btn-primary btn-sm"><i class="fas fa-arrow-circle-right"></i> {% trans "Go" %}</button>
|
|
||||||
</p>
|
|
||||||
{% csrf_token %}</form>
|
|
||||||
</div>
|
|
||||||
<!-- /.panel-body -->
|
|
||||||
</div>
|
|
||||||
<!-- /.panel -->
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
@ -267,8 +323,8 @@
|
|||||||
var name = data.split(" ")[1];
|
var name = data.split(" ")[1];
|
||||||
if (type === 'display')
|
if (type === 'display')
|
||||||
{
|
{
|
||||||
data = '<div class="tickettitle"><a href="' + get_url(row) + '" >' +
|
data = '<div class="tickettitle"><a href="' + get_url(row) + '" >' +
|
||||||
row.id + '. ' +
|
row.id + '. ' +
|
||||||
row.title + '</a></div>';
|
row.title + '</a></div>';
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
@ -297,12 +353,13 @@
|
|||||||
"render": function(data, type, row, meta) {
|
"render": function(data, type, row, meta) {
|
||||||
if (data != "None") {
|
if (data != "None") {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{"data": "submitter"},
|
||||||
{"data": "time_spent"},
|
{"data": "time_spent"},
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@ -345,6 +402,9 @@
|
|||||||
{% if query_params.search_string %}
|
{% if query_params.search_string %}
|
||||||
$("#filterBuilderSelect-Keywords")[0].disabled = "disabled";
|
$("#filterBuilderSelect-Keywords")[0].disabled = "disabled";
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if query_params.filtering.kbitem__in %}
|
||||||
|
$("#filterBuilderSelect-KBItems")[0].disabled = "disabled";
|
||||||
|
{% endif %}
|
||||||
});
|
});
|
||||||
|
|
||||||
{% for f in query_params.filtering %}
|
{% for f in query_params.filtering %}
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
{% load i18n humanize %}
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{% for ticket in tickets %}
|
|
||||||
<tr class="{{ ticket.get_priority_css_class }}">
|
|
||||||
<th><a href='{{ ticket.get_absolute_url }}'>{{ ticket.ticket }}</a></th>
|
|
||||||
<td><input type='checkbox' name='ticket_id' value='{{ ticket.id }}' class='ticket_multi_select' /></td>
|
|
||||||
<td>{{ ticket.priority }}|||||</td>
|
|
||||||
<th><a href='{{ ticket.get_absolute_url }}'>{{ ticket.title }}</a></th>
|
|
||||||
<td>{{ ticket.queue }}</td>
|
|
||||||
<td>{{ ticket.get_status }}</td>
|
|
||||||
<td data-order='{{ ticket.created|date:"U" }}'><span title='{{ ticket.created|date:"r" }}'>{{ ticket.created|naturaltime }}</span></td>
|
|
||||||
<td data-order='{{ ticket.due_date|date:"U" }}'><span title='{{ ticket.due_date|date:"r" }}'>{{ ticket.due_date|naturaltime }}</span></td>
|
|
||||||
<td>{{ ticket.get_assigned_to }}</td>
|
|
||||||
<td>{{ ticket.time_spent_formated }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
@ -94,7 +94,11 @@ class AttachmentUnitTests(TestCase):
|
|||||||
'content-type': 'text/utf8',
|
'content-type': 'text/utf8',
|
||||||
}
|
}
|
||||||
self.test_file = SimpleUploadedFile.from_dict(self.file_attrs)
|
self.test_file = SimpleUploadedFile.from_dict(self.file_attrs)
|
||||||
self.follow_up = models.FollowUp(ticket=models.Ticket(queue=models.Queue()))
|
self.follow_up = models.FollowUp.objects.create(
|
||||||
|
ticket=models.Ticket.objects.create(
|
||||||
|
queue=models.Queue.objects.create()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@mock.patch('helpdesk.lib.FollowUpAttachment', autospec=True)
|
@mock.patch('helpdesk.lib.FollowUpAttachment', autospec=True)
|
||||||
def test_unicode_attachment_filename(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
|
def test_unicode_attachment_filename(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
|
||||||
@ -109,19 +113,16 @@ class AttachmentUnitTests(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(filename, self.file_attrs['filename'])
|
self.assertEqual(filename, self.file_attrs['filename'])
|
||||||
|
|
||||||
# TODO: FIXME: what's wrong with this test that we get integrity errors?
|
@mock.patch('helpdesk.lib.FollowUpAttachment', autospec=True)
|
||||||
# @mock.patch('helpdesk.lib.FollowUpAttachment', autospec=True)
|
def test_autofill(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
|
||||||
# def test_autofill(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
|
""" check utf-8 data is parsed correctly """
|
||||||
# """ check utf-8 data is parsed correctly """
|
obj = models.FollowUpAttachment.objects.create(
|
||||||
# self.follow_up.pk = 100
|
followup=self.follow_up,
|
||||||
# self.follow_up.save()
|
file=self.test_file
|
||||||
# obj = models.FollowUpAttachment.objects.create(
|
)
|
||||||
# followup=self.follow_up,
|
self.assertEqual(obj.filename, self.file_attrs['filename'])
|
||||||
# file=self.test_file
|
self.assertEqual(obj.size, len(self.file_attrs['content']))
|
||||||
# )
|
self.assertEqual(obj.mime_type, "text/plain")
|
||||||
# self.assertEqual(obj.filename, self.file_attrs['filename'])
|
|
||||||
# self.assertEqual(obj.size, len(self.file_attrs['content']))
|
|
||||||
# self.assertEqual(obj.mime_type, "text/plain")
|
|
||||||
|
|
||||||
def test_kbi_attachment(self, mock_att_save, mock_queue_save, mock_ticket_save):
|
def test_kbi_attachment(self, mock_att_save, mock_queue_save, mock_ticket_save):
|
||||||
""" check utf-8 data is parsed correctly """
|
""" check utf-8 data is parsed correctly """
|
||||||
|
80
helpdesk/tests/test_kb.py
Normal file
80
helpdesk/tests/test_kb.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from helpdesk.models import KBCategory, KBItem, Queue, Ticket
|
||||||
|
|
||||||
|
from helpdesk.tests.helpers import (get_staff_user, reload_urlconf, User, create_ticket, print_response)
|
||||||
|
|
||||||
|
|
||||||
|
class KBTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.queue = Queue.objects.create(
|
||||||
|
title="Test queue",
|
||||||
|
slug="test_queue",
|
||||||
|
allow_public_submission=True,
|
||||||
|
)
|
||||||
|
self.queue.save()
|
||||||
|
cat = KBCategory.objects.create(
|
||||||
|
title="Test Cat",
|
||||||
|
slug="test_cat",
|
||||||
|
description="This is a test category",
|
||||||
|
queue=self.queue,
|
||||||
|
)
|
||||||
|
cat.save()
|
||||||
|
self.kbitem1 = KBItem.objects.create(
|
||||||
|
category=cat,
|
||||||
|
title="KBItem 1",
|
||||||
|
question="What?",
|
||||||
|
answer="A KB Item",
|
||||||
|
)
|
||||||
|
self.kbitem1.save()
|
||||||
|
self.kbitem2 = KBItem.objects.create(
|
||||||
|
category=cat,
|
||||||
|
title="KBItem 2",
|
||||||
|
question="When?",
|
||||||
|
answer="Now",
|
||||||
|
)
|
||||||
|
self.kbitem2.save()
|
||||||
|
self.user = get_staff_user()
|
||||||
|
|
||||||
|
def test_kb_index(self):
|
||||||
|
response = self.client.get(reverse('helpdesk:kb_index'))
|
||||||
|
self.assertContains(response, 'This is a test category')
|
||||||
|
|
||||||
|
def test_kb_category(self):
|
||||||
|
response = self.client.get(reverse('helpdesk:kb_category', args=("test_cat", )))
|
||||||
|
self.assertContains(response, 'This is a test category')
|
||||||
|
self.assertContains(response, 'KBItem 1')
|
||||||
|
self.assertContains(response, 'KBItem 2')
|
||||||
|
self.assertContains(response, 'Contact a human')
|
||||||
|
self.client.login(username=self.user.get_username(), password='password')
|
||||||
|
response = self.client.get(reverse('helpdesk:kb_category', args=("test_cat", )))
|
||||||
|
self.assertContains(response, '<i class="fa fa-thumbs-up fa-lg"></i>')
|
||||||
|
self.assertContains(response, '0 open tickets')
|
||||||
|
ticket = Ticket.objects.create(
|
||||||
|
title="Test ticket",
|
||||||
|
queue=self.queue,
|
||||||
|
kbitem=self.kbitem1,
|
||||||
|
)
|
||||||
|
ticket.save()
|
||||||
|
response = self.client.get(reverse('helpdesk:kb_category', args=("test_cat",)))
|
||||||
|
self.assertContains(response, '1 open tickets')
|
||||||
|
|
||||||
|
def test_kb_vote(self):
|
||||||
|
self.client.login(username=self.user.get_username(), password='password')
|
||||||
|
response = self.client.get(reverse('helpdesk:kb_vote', args=(self.kbitem1.pk,)) + "?vote=up")
|
||||||
|
cat_url = reverse('helpdesk:kb_category', args=("test_cat",)) + "?kbitem=1"
|
||||||
|
self.assertRedirects(response, cat_url)
|
||||||
|
response = self.client.get(cat_url)
|
||||||
|
self.assertContains(response, '1 people found this answer useful of 1')
|
||||||
|
response = self.client.get(reverse('helpdesk:kb_vote', args=(self.kbitem1.pk,)) + "?vote=down")
|
||||||
|
self.assertRedirects(response, cat_url)
|
||||||
|
response = self.client.get(cat_url)
|
||||||
|
self.assertContains(response, '0 people found this answer useful of 1')
|
||||||
|
|
||||||
|
def test_kb_category_iframe(self):
|
||||||
|
cat_url = reverse('helpdesk:kb_category', args=("test_cat",)) + "?kbitem=1;submitter_email=foo@bar.cz;title=lol;"
|
||||||
|
response = self.client.get(cat_url)
|
||||||
|
# Assert that query params are passed on to ticket submit form
|
||||||
|
self.assertContains(response, "'/helpdesk/tickets/submit/?queue=1;_readonly_fields_=queue;kbitem=1;submitter_email=foo%40bar.cz&title=lol")
|
@ -6,7 +6,7 @@ from django.test.client import Client
|
|||||||
|
|
||||||
from helpdesk.models import Queue, Ticket
|
from helpdesk.models import Queue, Ticket
|
||||||
from helpdesk import settings
|
from helpdesk import settings
|
||||||
from helpdesk.query import get_query
|
from helpdesk.query import __Query__
|
||||||
from helpdesk.user import HelpdeskUser
|
from helpdesk.user import HelpdeskUser
|
||||||
|
|
||||||
|
|
||||||
@ -166,7 +166,7 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
|||||||
for identifier in self.IDENTIFIERS:
|
for identifier in self.IDENTIFIERS:
|
||||||
self.client.login(username='User_%d' % identifier, password=str(identifier))
|
self.client.login(username='User_%d' % identifier, password=str(identifier))
|
||||||
response = self.client.get(reverse('helpdesk:list'))
|
response = self.client.get(reverse('helpdesk:list'))
|
||||||
tickets = get_query(response.context['urlsafe_query'], HelpdeskUser(self.identifier_users[identifier]))
|
tickets = __Query__(HelpdeskUser(self.identifier_users[identifier]), base64query=response.context['urlsafe_query']).get()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(tickets),
|
len(tickets),
|
||||||
identifier * 2,
|
identifier * 2,
|
||||||
@ -186,7 +186,7 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
|||||||
# Superuser
|
# Superuser
|
||||||
self.client.login(username='superuser', password='superuser')
|
self.client.login(username='superuser', password='superuser')
|
||||||
response = self.client.get(reverse('helpdesk:list'))
|
response = self.client.get(reverse('helpdesk:list'))
|
||||||
tickets = get_query(response.context['urlsafe_query'], HelpdeskUser(self.superuser))
|
tickets = __Query__(HelpdeskUser(self.superuser), base64query=response.context['urlsafe_query']).get()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(tickets),
|
len(tickets),
|
||||||
6,
|
6,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import email
|
import email
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from helpdesk.models import Queue, CustomField, FollowUp, Ticket, TicketCC
|
from helpdesk.models import Queue, CustomField, FollowUp, Ticket, TicketCC, KBCategory, KBItem
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
@ -11,6 +11,7 @@ from django.test.client import Client
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from helpdesk.email import object_from_message, create_ticket_cc
|
from helpdesk.email import object_from_message, create_ticket_cc
|
||||||
|
from helpdesk.tests.helpers import print_response
|
||||||
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@ -139,6 +140,37 @@ class TicketBasicsTestCase(TestCase):
|
|||||||
# Ensure only two e-mails were sent - submitter & updated.
|
# Ensure only two e-mails were sent - submitter & updated.
|
||||||
self.assertEqual(email_count + 2, len(mail.outbox))
|
self.assertEqual(email_count + 2, len(mail.outbox))
|
||||||
|
|
||||||
|
def test_create_ticket_public_no_loopback(self):
|
||||||
|
"""
|
||||||
|
Don't send emails to the queue's own inbox. It'll create a loop.
|
||||||
|
"""
|
||||||
|
email_count = len(mail.outbox)
|
||||||
|
|
||||||
|
self.queue_public.email_address = "queue@example.com"
|
||||||
|
self.queue_public.save()
|
||||||
|
|
||||||
|
post_data = {
|
||||||
|
'title': 'Test ticket title',
|
||||||
|
'queue': self.queue_public.id,
|
||||||
|
'submitter_email': 'queue@example.com',
|
||||||
|
'body': 'Test ticket body',
|
||||||
|
'priority': 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(reverse('helpdesk:home'), post_data, follow=True)
|
||||||
|
last_redirect = response.redirect_chain[-1]
|
||||||
|
last_redirect_url = last_redirect[0]
|
||||||
|
# last_redirect_status = last_redirect[1]
|
||||||
|
|
||||||
|
# Ensure we landed on the "View" page.
|
||||||
|
# Django 1.9 compatible way of testing this
|
||||||
|
# https://docs.djangoproject.com/en/1.9/releases/1.9/#http-redirects-no-longer-forced-to-absolute-uris
|
||||||
|
urlparts = urlparse(last_redirect_url)
|
||||||
|
self.assertEqual(urlparts.path, reverse('helpdesk:public_view'))
|
||||||
|
|
||||||
|
# Ensure submitter, new-queue + update-queue were all emailed.
|
||||||
|
self.assertEqual(email_count + 2, len(mail.outbox))
|
||||||
|
|
||||||
|
|
||||||
class EmailInteractionsTestCase(TestCase):
|
class EmailInteractionsTestCase(TestCase):
|
||||||
fixtures = ['emailtemplate.json']
|
fixtures = ['emailtemplate.json']
|
||||||
@ -976,3 +1008,24 @@ class EmailInteractionsTestCase(TestCase):
|
|||||||
# public_update_queue: +1
|
# public_update_queue: +1
|
||||||
expected_email_count += 1 + 2 + 1
|
expected_email_count += 1 + 2 + 1
|
||||||
self.assertEqual(expected_email_count, len(mail.outbox))
|
self.assertEqual(expected_email_count, len(mail.outbox))
|
||||||
|
|
||||||
|
def test_ticket_field_autofill(self):
|
||||||
|
cat = KBCategory.objects.create(
|
||||||
|
title="Test Cat",
|
||||||
|
slug="test_cat",
|
||||||
|
description="This is a test category",
|
||||||
|
queue=self.queue_public,
|
||||||
|
)
|
||||||
|
cat.save()
|
||||||
|
self.kbitem1 = KBItem.objects.create(
|
||||||
|
category=cat,
|
||||||
|
title="KBItem 1",
|
||||||
|
question="What?",
|
||||||
|
answer="A KB Item",
|
||||||
|
)
|
||||||
|
self.kbitem1.save()
|
||||||
|
cat_url = reverse('helpdesk:submit') + "?kbitem=1;submitter_email=foo@bar.cz;title=lol;"
|
||||||
|
response = self.client.get(cat_url)
|
||||||
|
self.assertContains(response, '<option value="1" selected>KBItem 1</option>')
|
||||||
|
self.assertContains(response, '<input type="email" name="submitter_email" value="foo@bar.cz" class="form-control form-control" required id="id_submitter_email">')
|
||||||
|
self.assertContains(response, '<input type="text" name="title" value="lol" class="form-control form-control" maxlength="100" required id="id_title">')
|
||||||
|
@ -151,6 +151,11 @@ urlpatterns = [
|
|||||||
url(r'^datatables_ticket_list/(?P<query>{})$'.format(base64_pattern),
|
url(r'^datatables_ticket_list/(?P<query>{})$'.format(base64_pattern),
|
||||||
staff.datatables_ticket_list,
|
staff.datatables_ticket_list,
|
||||||
name="datatables_ticket_list"),
|
name="datatables_ticket_list"),
|
||||||
|
|
||||||
|
url(r'^timeline_ticket_list/(?P<query>{})$'.format(base64_pattern),
|
||||||
|
staff.timeline_ticket_list,
|
||||||
|
name="timeline_ticket_list"),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
@ -162,6 +167,14 @@ urlpatterns += [
|
|||||||
public.create_ticket,
|
public.create_ticket,
|
||||||
name='submit'),
|
name='submit'),
|
||||||
|
|
||||||
|
url(r'^tickets/submit_iframe/$',
|
||||||
|
public.CreateTicketIframeView.as_view(),
|
||||||
|
name='submit_iframe'),
|
||||||
|
|
||||||
|
url(r'^tickets/success_iframe/$', # Ticket was submitted successfully
|
||||||
|
public.SuccessIframeView.as_view(),
|
||||||
|
name='success_iframe'),
|
||||||
|
|
||||||
url(r'^view/$',
|
url(r'^view/$',
|
||||||
public.view_ticket,
|
public.view_ticket,
|
||||||
name='public_view'),
|
name='public_view'),
|
||||||
@ -223,17 +236,17 @@ if helpdesk_settings.HELPDESK_KB_ENABLED:
|
|||||||
kb.index,
|
kb.index,
|
||||||
name='kb_index'),
|
name='kb_index'),
|
||||||
|
|
||||||
url(r'^kb/(?P<item>[0-9]+)/$',
|
url(r'^kb/(?P<slug>[A-Za-z0-9_-]+)/$',
|
||||||
kb.item,
|
kb.category,
|
||||||
name='kb_item'),
|
name='kb_category'),
|
||||||
|
|
||||||
url(r'^kb/(?P<item>[0-9]+)/vote/$',
|
url(r'^kb/(?P<item>[0-9]+)/vote/$',
|
||||||
kb.vote,
|
kb.vote,
|
||||||
name='kb_vote'),
|
name='kb_vote'),
|
||||||
|
|
||||||
url(r'^kb/(?P<slug>[A-Za-z0-9_-]+)/$',
|
url(r'^kb_iframe/(?P<slug>[A-Za-z0-9_-]+)/$',
|
||||||
kb.category,
|
kb.category_iframe,
|
||||||
name='kb_category'),
|
name='kb_category_iframe'),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
|
36
helpdesk/views/abstract_views.py
Normal file
36
helpdesk/views/abstract_views.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from django.views.generic.edit import FormView
|
||||||
|
|
||||||
|
from helpdesk.models import CustomField, KBItem, Queue
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractCreateTicketMixin():
|
||||||
|
def get_initial(self):
|
||||||
|
initial_data = {}
|
||||||
|
request = self.request
|
||||||
|
try:
|
||||||
|
initial_data['queue'] = Queue.objects.get(slug=request.GET.get('queue', None)).id
|
||||||
|
except Queue.DoesNotExist:
|
||||||
|
pass
|
||||||
|
if request.user.is_authenticated and request.user.usersettings_helpdesk.use_email_as_submitter and request.user.email:
|
||||||
|
initial_data['submitter_email'] = request.user.email
|
||||||
|
|
||||||
|
query_param_fields = ['submitter_email', 'title', 'body', 'queue', 'kbitem']
|
||||||
|
custom_fields = ["custom_%s" % f.name for f in CustomField.objects.filter(staff_only=False)]
|
||||||
|
query_param_fields += custom_fields
|
||||||
|
for qpf in query_param_fields:
|
||||||
|
initial_data[qpf] = request.GET.get(qpf, initial_data.get(qpf, ""))
|
||||||
|
|
||||||
|
return initial_data
|
||||||
|
|
||||||
|
def get_form_kwargs(self, *args, **kwargs):
|
||||||
|
kwargs = super().get_form_kwargs(*args, **kwargs)
|
||||||
|
kbitem = self.request.GET.get(
|
||||||
|
'kbitem',
|
||||||
|
self.request.POST.get('kbitem', None),
|
||||||
|
)
|
||||||
|
if kbitem:
|
||||||
|
try:
|
||||||
|
kwargs['kbcategory'] = KBItem.objects.get(pk=int(kbitem)).category
|
||||||
|
except (ValueError, KBItem.DoesNotExist):
|
||||||
|
pass
|
||||||
|
return kwargs
|
@ -10,6 +10,7 @@ views/kb.py - Public-facing knowledgebase views. The knowledgebase is a
|
|||||||
|
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
|
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||||
|
|
||||||
from helpdesk import settings as helpdesk_settings
|
from helpdesk import settings as helpdesk_settings
|
||||||
from helpdesk.models import KBCategory, KBItem
|
from helpdesk.models import KBCategory, KBItem
|
||||||
@ -24,34 +25,57 @@ def index(request):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def category(request, slug):
|
def category(request, slug, iframe=False):
|
||||||
category = get_object_or_404(KBCategory, slug__iexact=slug)
|
category = get_object_or_404(KBCategory, slug__iexact=slug)
|
||||||
items = category.kbitem_set.all()
|
items = category.kbitem_set.all()
|
||||||
return render(request, 'helpdesk/kb_category.html', {
|
selected_item = request.GET.get('kbitem', None)
|
||||||
|
try:
|
||||||
|
selected_item = int(selected_item)
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
qparams = request.GET.copy()
|
||||||
|
try:
|
||||||
|
del qparams['kbitem']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
template = 'helpdesk/kb_category.html'
|
||||||
|
if iframe:
|
||||||
|
template = 'helpdesk/kb_category_iframe.html'
|
||||||
|
staff = request.user.is_authenticated and request.user.is_staff
|
||||||
|
return render(request, template, {
|
||||||
'category': category,
|
'category': category,
|
||||||
'items': items,
|
'items': items,
|
||||||
|
'selected_item': selected_item,
|
||||||
|
'query_param_string': qparams.urlencode(),
|
||||||
'helpdesk_settings': helpdesk_settings,
|
'helpdesk_settings': helpdesk_settings,
|
||||||
|
'iframe': iframe,
|
||||||
|
'staff': staff,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def item(request, item):
|
@xframe_options_exempt
|
||||||
item = get_object_or_404(KBItem, pk=item)
|
def category_iframe(request, slug):
|
||||||
return render(request, 'helpdesk/kb_item.html', {
|
return category(request, slug, iframe=True)
|
||||||
'category': item.category,
|
|
||||||
'item': item,
|
|
||||||
'helpdesk_settings': helpdesk_settings,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def vote(request, item):
|
def vote(request, item):
|
||||||
item = get_object_or_404(KBItem, pk=item)
|
item = get_object_or_404(KBItem, pk=item)
|
||||||
vote = request.GET.get('vote', None)
|
vote = request.GET.get('vote', None)
|
||||||
if vote in ('up', 'down'):
|
if vote == 'up':
|
||||||
if request.user not in item.voted_by:
|
if not item.voted_by.filter(pk=request.user.pk):
|
||||||
|
|
||||||
item.votes += 1
|
item.votes += 1
|
||||||
if vote == 'up':
|
item.voted_by.add(request.user.pk)
|
||||||
item.recommendations += 1
|
item.recommendations += 1
|
||||||
item.save()
|
if item.downvoted_by.filter(pk=request.user.pk):
|
||||||
|
item.votes -= 1
|
||||||
|
item.downvoted_by.remove(request.user.pk)
|
||||||
|
if vote == 'down':
|
||||||
|
if not item.downvoted_by.filter(pk=request.user.pk):
|
||||||
|
item.votes += 1
|
||||||
|
item.downvoted_by.add(request.user.pk)
|
||||||
|
item.recommendations -= 1
|
||||||
|
if item.voted_by.filter(pk=request.user.pk):
|
||||||
|
item.votes -= 1
|
||||||
|
item.voted_by.remove(request.user.pk)
|
||||||
|
item.save()
|
||||||
return HttpResponseRedirect(item.get_absolute_url())
|
return HttpResponseRedirect(item.get_absolute_url())
|
||||||
|
@ -7,26 +7,24 @@ views/public.py - All public facing views, eg non-staff (no authentication
|
|||||||
required) views.
|
required) views.
|
||||||
"""
|
"""
|
||||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||||
try:
|
from django.urls import reverse
|
||||||
# Django 2.0+
|
|
||||||
from django.urls import reverse
|
|
||||||
except ImportError:
|
|
||||||
# Django < 2
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.http import HttpResponseRedirect, HttpResponse
|
from django.http import HttpResponseRedirect, HttpResponse
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.utils.http import urlquote
|
from django.utils.http import urlquote
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
from django.views.generic.edit import FormView
|
from django.views.generic.edit import FormView
|
||||||
|
|
||||||
from helpdesk import settings as helpdesk_settings
|
from helpdesk import settings as helpdesk_settings
|
||||||
from helpdesk.decorators import protect_view, is_helpdesk_staff
|
from helpdesk.decorators import protect_view, is_helpdesk_staff
|
||||||
import helpdesk.views.staff as staff
|
import helpdesk.views.staff as staff
|
||||||
|
import helpdesk.views.abstract_views as abstract_views
|
||||||
from helpdesk.forms import PublicTicketForm
|
from helpdesk.forms import PublicTicketForm
|
||||||
from helpdesk.lib import text_is_spam
|
from helpdesk.lib import text_is_spam
|
||||||
from helpdesk.models import Ticket, Queue, UserSettings, KBCategory
|
from helpdesk.models import CustomField, Ticket, Queue, UserSettings, KBCategory, KBItem
|
||||||
|
|
||||||
|
|
||||||
def create_ticket(request, *args, **kwargs):
|
def create_ticket(request, *args, **kwargs):
|
||||||
@ -36,8 +34,7 @@ def create_ticket(request, *args, **kwargs):
|
|||||||
return CreateTicketView.as_view()(request, *args, **kwargs)
|
return CreateTicketView.as_view()(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class CreateTicketView(FormView):
|
class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
|
||||||
template_name = 'helpdesk/public_create_ticket.html'
|
|
||||||
form_class = PublicTicketForm
|
form_class = PublicTicketForm
|
||||||
|
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
@ -57,42 +54,29 @@ class CreateTicketView(FormView):
|
|||||||
return HttpResponseRedirect(reverse('helpdesk:dashboard'))
|
return HttpResponseRedirect(reverse('helpdesk:dashboard'))
|
||||||
return super().dispatch(*args, **kwargs)
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context['kb_categories'] = KBCategory.objects.all()
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
request = self.request
|
request = self.request
|
||||||
initial_data = {}
|
initial_data = super().get_initial()
|
||||||
try:
|
|
||||||
queue = Queue.objects.get(slug=request.GET.get('queue', None))
|
|
||||||
except Queue.DoesNotExist:
|
|
||||||
queue = None
|
|
||||||
|
|
||||||
# add pre-defined data for public ticket
|
# add pre-defined data for public ticket
|
||||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE'):
|
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE'):
|
||||||
# get the requested queue; return an error if queue not found
|
# get the requested queue; return an error if queue not found
|
||||||
try:
|
try:
|
||||||
queue = Queue.objects.get(slug=settings.HELPDESK_PUBLIC_TICKET_QUEUE)
|
initial_data['queue'] = Queue.objects.get(slug=settings.HELPDESK_PUBLIC_TICKET_QUEUE).id
|
||||||
except Queue.DoesNotExist:
|
except Queue.DoesNotExist:
|
||||||
return HttpResponse(status=500)
|
return HttpResponse(status=500)
|
||||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'):
|
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'):
|
||||||
initial_data['priority'] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY
|
initial_data['priority'] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY
|
||||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_DUE_DATE'):
|
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_DUE_DATE'):
|
||||||
initial_data['due_date'] = settings.HELPDESK_PUBLIC_TICKET_DUE_DATE
|
initial_data['due_date'] = settings.HELPDESK_PUBLIC_TICKET_DUE_DATE
|
||||||
|
|
||||||
if queue:
|
|
||||||
initial_data['queue'] = queue.id
|
|
||||||
|
|
||||||
if request.user.is_authenticated and request.user.email:
|
|
||||||
initial_data['submitter_email'] = request.user.email
|
|
||||||
|
|
||||||
query_param_fields = ['submitter_email', 'title', 'body']
|
|
||||||
for qpf in query_param_fields:
|
|
||||||
initial_data[qpf] = request.GET.get(qpf, initial_data.get(qpf, ""))
|
|
||||||
return initial_data
|
return initial_data
|
||||||
|
|
||||||
|
def get_form_kwargs(self, *args, **kwargs):
|
||||||
|
kwargs = super().get_form_kwargs(*args, **kwargs)
|
||||||
|
kwargs['hidden_fields'] = self.request.GET.get('_hide_fields_', '').split(',')
|
||||||
|
kwargs['readonly_fields'] = self.request.GET.get('_readonly_fields_', '').split(',')
|
||||||
|
return kwargs
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
request = self.request
|
request = self.request
|
||||||
if text_is_spam(form.cleaned_data['body'], request):
|
if text_is_spam(form.cleaned_data['body'], request):
|
||||||
@ -115,9 +99,39 @@ class CreateTicketView(FormView):
|
|||||||
request = self.request
|
request = self.request
|
||||||
|
|
||||||
|
|
||||||
|
class CreateTicketIframeView(BaseCreateTicketView):
|
||||||
|
template_name = 'helpdesk/public_create_ticket_iframe.html'
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@xframe_options_exempt
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
if super().form_valid(form).status_code == 302:
|
||||||
|
return HttpResponseRedirect(reverse('helpdesk:success_iframe'))
|
||||||
|
|
||||||
|
|
||||||
|
class SuccessIframeView(TemplateView):
|
||||||
|
template_name = 'helpdesk/success_iframe.html'
|
||||||
|
|
||||||
|
@xframe_options_exempt
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class CreateTicketView(BaseCreateTicketView):
|
||||||
|
template_name = 'helpdesk/public_create_ticket.html'
|
||||||
|
|
||||||
|
|
||||||
class Homepage(CreateTicketView):
|
class Homepage(CreateTicketView):
|
||||||
template_name = 'helpdesk/public_homepage.html'
|
template_name = 'helpdesk/public_homepage.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['kb_categories'] = KBCategory.objects.all()
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
def search_for_ticket(request, error_message=None):
|
def search_for_ticket(request, error_message=None):
|
||||||
if hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC:
|
if hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC:
|
||||||
|
@ -27,18 +27,14 @@ from django.utils import timezone
|
|||||||
from django.views.generic.edit import FormView, UpdateView
|
from django.views.generic.edit import FormView, UpdateView
|
||||||
|
|
||||||
from helpdesk.query import (
|
from helpdesk.query import (
|
||||||
|
get_query_class,
|
||||||
query_to_dict,
|
query_to_dict,
|
||||||
get_query,
|
|
||||||
apply_query,
|
|
||||||
query_tickets_by_args,
|
|
||||||
query_to_base64,
|
query_to_base64,
|
||||||
query_from_base64,
|
query_from_base64,
|
||||||
)
|
)
|
||||||
|
|
||||||
from helpdesk.user import HelpdeskUser
|
from helpdesk.user import HelpdeskUser
|
||||||
|
|
||||||
from helpdesk.serializers import DatatablesTicketSerializer
|
|
||||||
|
|
||||||
from helpdesk.decorators import (
|
from helpdesk.decorators import (
|
||||||
helpdesk_staff_member_required, helpdesk_superuser_required,
|
helpdesk_staff_member_required, helpdesk_superuser_required,
|
||||||
is_helpdesk_staff
|
is_helpdesk_staff
|
||||||
@ -56,9 +52,10 @@ from helpdesk.lib import (
|
|||||||
)
|
)
|
||||||
from helpdesk.models import (
|
from helpdesk.models import (
|
||||||
Ticket, Queue, FollowUp, TicketChange, PreSetReply, FollowUpAttachment, SavedSearch,
|
Ticket, Queue, FollowUp, TicketChange, PreSetReply, FollowUpAttachment, SavedSearch,
|
||||||
IgnoreEmail, TicketCC, TicketDependency, UserSettings,
|
IgnoreEmail, TicketCC, TicketDependency, UserSettings, KBItem,
|
||||||
)
|
)
|
||||||
from helpdesk import settings as helpdesk_settings
|
from helpdesk import settings as helpdesk_settings
|
||||||
|
import helpdesk.views.abstract_views as abstract_views
|
||||||
from helpdesk.views.permissions import MustBeStaffMixin
|
from helpdesk.views.permissions import MustBeStaffMixin
|
||||||
from ..lib import format_time_spent
|
from ..lib import format_time_spent
|
||||||
|
|
||||||
@ -71,6 +68,7 @@ import re
|
|||||||
|
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
Query = get_query_class()
|
||||||
|
|
||||||
if helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE:
|
if helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE:
|
||||||
# treat 'normal' users like 'staff'
|
# treat 'normal' users like 'staff'
|
||||||
@ -805,7 +803,9 @@ def ticket_list(request):
|
|||||||
'search_string': '',
|
'search_string': '',
|
||||||
}
|
}
|
||||||
default_query_params = {
|
default_query_params = {
|
||||||
'filtering': {'status__in': [1, 2, 3]},
|
'filtering': {
|
||||||
|
'status__in': [1, 2, 3],
|
||||||
|
},
|
||||||
'sorting': 'created',
|
'sorting': 'created',
|
||||||
'search_string': '',
|
'search_string': '',
|
||||||
'sortreverse': False,
|
'sortreverse': False,
|
||||||
@ -852,7 +852,7 @@ def ticket_list(request):
|
|||||||
|
|
||||||
if saved_query:
|
if saved_query:
|
||||||
pass
|
pass
|
||||||
elif not {'queue', 'assigned_to', 'status', 'q', 'sort', 'sortreverse'}.intersection(request.GET):
|
elif not {'queue', 'assigned_to', 'status', 'q', 'sort', 'sortreverse', 'kbitem'}.intersection(request.GET):
|
||||||
# Fall-back if no querying is being done
|
# Fall-back if no querying is being done
|
||||||
all_queues = Queue.objects.all()
|
all_queues = Queue.objects.all()
|
||||||
query_params = deepcopy(default_query_params)
|
query_params = deepcopy(default_query_params)
|
||||||
@ -861,6 +861,7 @@ def ticket_list(request):
|
|||||||
('queue', 'queue__id__in'),
|
('queue', 'queue__id__in'),
|
||||||
('assigned_to', 'assigned_to__id__in'),
|
('assigned_to', 'assigned_to__id__in'),
|
||||||
('status', 'status__in'),
|
('status', 'status__in'),
|
||||||
|
('kbitem', 'kbitem__in'),
|
||||||
]
|
]
|
||||||
|
|
||||||
for param, filter_command in filter_in_params:
|
for param, filter_command in filter_in_params:
|
||||||
@ -887,7 +888,7 @@ def ticket_list(request):
|
|||||||
|
|
||||||
# SORTING
|
# SORTING
|
||||||
sort = request.GET.get('sort', None)
|
sort = request.GET.get('sort', None)
|
||||||
if sort not in ('status', 'assigned_to', 'created', 'title', 'queue', 'priority'):
|
if sort not in ('status', 'assigned_to', 'created', 'title', 'queue', 'priority', 'kbitem'):
|
||||||
sort = 'created'
|
sort = 'created'
|
||||||
query_params['sorting'] = sort
|
query_params['sorting'] = sort
|
||||||
|
|
||||||
@ -896,7 +897,7 @@ def ticket_list(request):
|
|||||||
|
|
||||||
urlsafe_query = query_to_base64(query_params)
|
urlsafe_query = query_to_base64(query_params)
|
||||||
|
|
||||||
get_query(urlsafe_query, huser)
|
Query(huser, base64query=urlsafe_query).refresh_query()
|
||||||
|
|
||||||
user_saved_queries = SavedSearch.objects.filter(Q(user=request.user) | Q(shared__exact=True))
|
user_saved_queries = SavedSearch.objects.filter(Q(user=request.user) | Q(shared__exact=True))
|
||||||
|
|
||||||
@ -910,12 +911,15 @@ def ticket_list(request):
|
|||||||
'<a href="http://docs.djangoproject.com/en/dev/ref/databases/#sqlite-string-matching">'
|
'<a href="http://docs.djangoproject.com/en/dev/ref/databases/#sqlite-string-matching">'
|
||||||
'Django Documentation on string matching in SQLite</a>.')
|
'Django Documentation on string matching in SQLite</a>.')
|
||||||
|
|
||||||
|
kbitem_choices = [(item.pk, item.title) for item in KBItem.objects.all()]
|
||||||
|
|
||||||
return render(request, 'helpdesk/ticket_list.html', dict(
|
return render(request, 'helpdesk/ticket_list.html', dict(
|
||||||
context,
|
context,
|
||||||
default_tickets_per_page=request.user.usersettings_helpdesk.tickets_per_page,
|
default_tickets_per_page=request.user.usersettings_helpdesk.tickets_per_page,
|
||||||
user_choices=User.objects.filter(is_active=True, is_staff=True),
|
user_choices=User.objects.filter(is_active=True, is_staff=True),
|
||||||
queue_choices=huser.get_queues(),
|
queue_choices=huser.get_queues(),
|
||||||
status_choices=Ticket.STATUS_CHOICES,
|
status_choices=Ticket.STATUS_CHOICES,
|
||||||
|
kbitem_choices=kbitem_choices,
|
||||||
urlsafe_query=urlsafe_query,
|
urlsafe_query=urlsafe_query,
|
||||||
user_saved_queries=user_saved_queries,
|
user_saved_queries=user_saved_queries,
|
||||||
query_params=query_params,
|
query_params=query_params,
|
||||||
@ -964,17 +968,18 @@ def datatables_ticket_list(request, query):
|
|||||||
on the table. query_tickets_by_args is at lib.py, DatatablesTicketSerializer is in
|
on the table. query_tickets_by_args is at lib.py, DatatablesTicketSerializer is in
|
||||||
serializers.py. The serializers and this view use django-rest_framework methods
|
serializers.py. The serializers and this view use django-rest_framework methods
|
||||||
"""
|
"""
|
||||||
objects = get_query(query, HelpdeskUser(request.user))
|
query = Query(HelpdeskUser(request.user), base64query=query)
|
||||||
model_object = query_tickets_by_args(objects, '-date_created', **request.query_params)
|
result = query.get_datatables_context(**request.query_params)
|
||||||
serializer = DatatablesTicketSerializer(model_object['items'], many=True)
|
|
||||||
result = dict()
|
|
||||||
result['data'] = serializer.data
|
|
||||||
result['draw'] = model_object['draw']
|
|
||||||
result['recordsTotal'] = model_object['total']
|
|
||||||
result['recordsFiltered'] = model_object['count']
|
|
||||||
return (JsonResponse(result, status=status.HTTP_200_OK))
|
return (JsonResponse(result, status=status.HTTP_200_OK))
|
||||||
|
|
||||||
|
|
||||||
|
@helpdesk_staff_member_required
|
||||||
|
@api_view(['GET'])
|
||||||
|
def timeline_ticket_list(request, query):
|
||||||
|
query = Query(HelpdeskUser(request.user), base64query=query)
|
||||||
|
return (JsonResponse(query.get_timeline_context(), status=status.HTTP_200_OK))
|
||||||
|
|
||||||
|
|
||||||
@helpdesk_staff_member_required
|
@helpdesk_staff_member_required
|
||||||
def edit_ticket(request, ticket_id):
|
def edit_ticket(request, ticket_id):
|
||||||
ticket = get_object_or_404(Ticket, id=ticket_id)
|
ticket = get_object_or_404(Ticket, id=ticket_id)
|
||||||
@ -994,17 +999,12 @@ def edit_ticket(request, ticket_id):
|
|||||||
edit_ticket = staff_member_required(edit_ticket)
|
edit_ticket = staff_member_required(edit_ticket)
|
||||||
|
|
||||||
|
|
||||||
class CreateTicketView(MustBeStaffMixin, FormView):
|
class CreateTicketView(MustBeStaffMixin, abstract_views.AbstractCreateTicketMixin, FormView):
|
||||||
template_name = 'helpdesk/create_ticket.html'
|
template_name = 'helpdesk/create_ticket.html'
|
||||||
form_class = TicketForm
|
form_class = TicketForm
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
initial_data = {}
|
initial_data = super().get_initial()
|
||||||
request = self.request
|
|
||||||
if request.user.usersettings_helpdesk.use_email_as_submitter and request.user.email:
|
|
||||||
initial_data['submitter_email'] = request.user.email
|
|
||||||
if 'queue' in request.GET:
|
|
||||||
initial_data['queue'] = request.GET['queue']
|
|
||||||
return initial_data
|
return initial_data
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
|
Loading…
Reference in New Issue
Block a user