mirror of
https://gitea.mueller.network/extern/django-helpdesk.git
synced 2024-12-26 08:39:08 +01:00
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`
|
||||
- `body`
|
||||
- `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.
|
||||
|
@ -72,7 +72,7 @@ class FollowUpAdmin(admin.ModelAdmin):
|
||||
class KBItemAdmin(admin.ModelAdmin):
|
||||
list_display = ('category', 'title', 'last_updated',)
|
||||
inlines = [KBIAttachmentInline]
|
||||
readonly_fields = ('voted_by',)
|
||||
readonly_fields = ('voted_by', 'downvoted_by')
|
||||
|
||||
list_display_links = ('title',)
|
||||
|
||||
|
@ -18,7 +18,7 @@ from django.utils import timezone
|
||||
|
||||
from helpdesk.lib import safe_template_context, process_attachments
|
||||
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
|
||||
|
||||
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.'),
|
||||
)
|
||||
|
||||
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):
|
||||
if staff_only_filter is None:
|
||||
queryset = CustomField.objects.all()
|
||||
@ -197,7 +207,10 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
return Queue.objects.get(id=int(self.cleaned_data['queue']))
|
||||
|
||||
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'],
|
||||
submitter_email=self.cleaned_data['submitter_email'],
|
||||
@ -207,6 +220,7 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
description=self.cleaned_data['body'],
|
||||
priority=self.cleaned_data['priority'],
|
||||
due_date=self.cleaned_data['due_date'],
|
||||
kbitem=kbitem,
|
||||
)
|
||||
|
||||
return ticket, queue
|
||||
@ -337,34 +351,28 @@ class PublicTicketForm(AbstractTicketForm):
|
||||
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
|
||||
"""
|
||||
super(PublicTicketForm, self).__init__(*args, **kwargs)
|
||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE'):
|
||||
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()
|
||||
self._add_form_custom_fields(False)
|
||||
|
||||
def _get_queue(self):
|
||||
if getattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE', None):
|
||||
# force queue to be the pre-defined one
|
||||
# (only for anon submissions)
|
||||
return Queue.objects.filter(
|
||||
slug=settings.HELPDESK_PUBLIC_TICKET_QUEUE
|
||||
).first()
|
||||
else:
|
||||
# get the queue user entered
|
||||
return Queue.objects.get(id=int(self.cleaned_data['queue']))
|
||||
field_hide_table = {
|
||||
'queue': 'HELPDESK_PUBLIC_TICKET_QUEUE',
|
||||
'priority': 'HELPDESK_PUBLIC_TICKET_PRIORITY',
|
||||
'due_date': 'HELPDESK_PUBLIC_TICKET_DUE_DATE',
|
||||
}
|
||||
|
||||
for field in self.fields.keys():
|
||||
setting = field_hide_table.get(field, None)
|
||||
if (setting and hasattr(settings, setting)) or field in hidden_fields:
|
||||
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):
|
||||
"""
|
||||
|
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,
|
||||
)
|
||||
|
||||
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
|
||||
def time_spent(self):
|
||||
"""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:
|
||||
recipients.update(dont_send_to)
|
||||
|
||||
recipients.add(self.queue.email_address)
|
||||
|
||||
def should_receive(email):
|
||||
return email and email not in recipients
|
||||
|
||||
@ -1216,6 +1226,14 @@ class KBCategory(models.Model):
|
||||
_('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):
|
||||
return '%s' % self.title
|
||||
|
||||
@ -1234,7 +1252,14 @@ class KBItem(models.Model):
|
||||
An item within the knowledgebase. Very straightforward question/answer
|
||||
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(
|
||||
KBCategory,
|
||||
on_delete=models.CASCADE,
|
||||
@ -1294,7 +1319,14 @@ class KBItem(models.Model):
|
||||
|
||||
def get_absolute_url(self):
|
||||
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):
|
||||
return get_markdown(self.answer)
|
||||
|
@ -1,12 +1,16 @@
|
||||
from django.db.models import Q
|
||||
from django.core.cache import cache
|
||||
|
||||
from model_utils import Choices
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from base64 import b64encode
|
||||
from base64 import b64decode
|
||||
import json
|
||||
|
||||
from model_utils import Choices
|
||||
|
||||
from helpdesk.serializers import DatatablesTicketSerializer
|
||||
|
||||
|
||||
def query_to_base64(query):
|
||||
"""
|
||||
@ -47,7 +51,64 @@ def query_to_dict(results, descriptions):
|
||||
return output
|
||||
|
||||
|
||||
def apply_query(queryset, params):
|
||||
def get_search_filter_args(search):
|
||||
if search.startswith('queue:'):
|
||||
return Q(queue__title__icontains=search[len('queue:'):])
|
||||
if search.startswith('priority:'):
|
||||
return Q(priority__icontains=search[len('priority:'):])
|
||||
filter = Q()
|
||||
for subsearch in search.split("OR"):
|
||||
subsearch = subsearch.strip()
|
||||
filter = (
|
||||
filter |
|
||||
Q(id__icontains=subsearch) |
|
||||
Q(title__icontains=subsearch) |
|
||||
Q(description__icontains=subsearch) |
|
||||
Q(priority__icontains=subsearch) |
|
||||
Q(resolution__icontains=subsearch) |
|
||||
Q(submitter_email__icontains=subsearch) |
|
||||
Q(assigned_to__email__icontains=subsearch) |
|
||||
Q(ticketcustomfieldvalue__value__icontains=subsearch) |
|
||||
Q(created__icontains=subsearch) |
|
||||
Q(due_date__icontains=subsearch)
|
||||
)
|
||||
return filter
|
||||
|
||||
|
||||
DATATABLES_ORDER_COLUMN_CHOICES = Choices(
|
||||
('0', 'id'),
|
||||
('2', 'priority'),
|
||||
('3', 'title'),
|
||||
('4', 'queue'),
|
||||
('5', 'status'),
|
||||
('6', 'created'),
|
||||
('7', 'due_date'),
|
||||
('8', 'assigned_to')
|
||||
)
|
||||
|
||||
|
||||
def get_query_class():
|
||||
from django.conf import settings
|
||||
|
||||
def _get_query_class():
|
||||
return __Query__
|
||||
return getattr(settings,
|
||||
'HELPDESK_QUERY_CLASS',
|
||||
_get_query_class)()
|
||||
|
||||
|
||||
class __Query__:
|
||||
def __init__(self, huser, base64query=None, query_params=None):
|
||||
self.huser = huser
|
||||
self.params = query_params if query_params else query_from_base64(base64query)
|
||||
self.base64 = base64query if base64query else query_to_base64(query_params)
|
||||
self.result = None
|
||||
|
||||
def get_search_filter_args(self):
|
||||
search = self.params.get('search_string', '')
|
||||
return get_search_filter_args(search)
|
||||
|
||||
def __run__(self, queryset):
|
||||
"""
|
||||
Apply a dict-based set of filters & parameters to a queryset.
|
||||
|
||||
@ -62,57 +123,35 @@ def apply_query(queryset, params):
|
||||
|
||||
sorting: The name of the column to sort by
|
||||
"""
|
||||
for key in params['filtering'].keys():
|
||||
filter = {key: params['filtering'][key]}
|
||||
for key in self.params.get('filtering', {}).keys():
|
||||
filter = {key: self.params['filtering'][key]}
|
||||
queryset = queryset.filter(**filter)
|
||||
|
||||
search = params.get('search_string', '')
|
||||
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)
|
||||
)
|
||||
|
||||
queryset = queryset.filter(qset)
|
||||
|
||||
sorting = params.get('sorting', None)
|
||||
queryset = queryset.filter(self.get_search_filter_args())
|
||||
sorting = self.params.get('sorting', None)
|
||||
if sorting:
|
||||
sortreverse = params.get('sortreverse', None)
|
||||
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
|
||||
|
||||
return queryset
|
||||
def get_cache_key(self):
|
||||
return str(self.huser.user.pk) + ":" + self.base64
|
||||
|
||||
|
||||
def get_query(query, huser):
|
||||
# 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)
|
||||
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()
|
||||
|
||||
ORDER_COLUMN_CHOICES = Choices(
|
||||
('0', 'id'),
|
||||
('2', 'priority'),
|
||||
('3', 'title'),
|
||||
('4', 'queue'),
|
||||
('5', 'status'),
|
||||
('6', 'created'),
|
||||
('7', 'due_date'),
|
||||
('8', 'assigned_to')
|
||||
)
|
||||
|
||||
|
||||
def query_tickets_by_args(objects, order_by, **kwargs):
|
||||
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
|
||||
@ -121,6 +160,8 @@ def query_tickets_by_args(objects, order_by, **kwargs):
|
||||
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])
|
||||
@ -128,7 +169,7 @@ def query_tickets_by_args(objects, order_by, **kwargs):
|
||||
order_column = kwargs.get('order[0][column]', None)[0]
|
||||
order = kwargs.get('order[0][dir]', None)[0]
|
||||
|
||||
order_column = ORDER_COLUMN_CHOICES[order_column]
|
||||
order_column = DATATABLES_ORDER_COLUMN_CHOICES[order_column]
|
||||
# django orm '-' -> desc
|
||||
if order == 'desc':
|
||||
order_column = '-' + order_column
|
||||
@ -137,20 +178,44 @@ def query_tickets_by_args(objects, order_by, **kwargs):
|
||||
total = queryset.count()
|
||||
|
||||
if search_value:
|
||||
queryset = queryset.filter(Q(id__icontains=search_value) |
|
||||
Q(priority__icontains=search_value) |
|
||||
Q(title__icontains=search_value) |
|
||||
Q(queue__title__icontains=search_value) |
|
||||
Q(status__icontains=search_value) |
|
||||
Q(created__icontains=search_value) |
|
||||
Q(due_date__icontains=search_value) |
|
||||
Q(assigned_to__email__icontains=search_value))
|
||||
queryset = queryset.filter(get_search_filter_args(search_value))
|
||||
|
||||
count = queryset.count()
|
||||
queryset = queryset.order_by(order_column)[start:start + length]
|
||||
return {
|
||||
'items': queryset,
|
||||
'count': count,
|
||||
'total': total,
|
||||
'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):
|
||||
ticket = serializers.SerializerMethodField()
|
||||
assigned_to = serializers.SerializerMethodField()
|
||||
submitter = serializers.SerializerMethodField()
|
||||
created = serializers.SerializerMethodField()
|
||||
due_date = serializers.SerializerMethodField()
|
||||
status = serializers.SerializerMethodField()
|
||||
@ -26,7 +27,7 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
|
||||
model = Ticket
|
||||
# fields = '__all__'
|
||||
fields = ('ticket', 'id', 'priority', 'title', 'queue', 'status',
|
||||
'created', 'due_date', 'assigned_to', 'row_class',
|
||||
'created', 'due_date', 'assigned_to', 'submitter', 'row_class',
|
||||
'time_spent')
|
||||
|
||||
def get_queue(self, obj):
|
||||
@ -53,6 +54,9 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
|
||||
else:
|
||||
return ("None")
|
||||
|
||||
def get_submitter(self, obj):
|
||||
return obj.submitter_email
|
||||
|
||||
def get_time_spent(self, obj):
|
||||
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>
|
||||
|
||||
<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" >
|
||||
|
||||
<!-- 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>
|
||||
|
||||
{% include 'helpdesk/base-head.html' %}
|
||||
{% block helpdesk_head %}{% endblock %}
|
||||
|
||||
</head>
|
||||
@ -107,33 +45,7 @@
|
||||
{% 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 -->
|
||||
<script src="{% static 'helpdesk/vendor/metisMenu/metisMenu.min.js' %}"></script>
|
||||
|
||||
<!-- Custom Theme JavaScript -->
|
||||
<script src="{% static 'helpdesk/js/sb-admin.js' %}"></script>
|
||||
|
||||
{% include 'helpdesk/base_js.html' %}
|
||||
{% block helpdesk_js %}{% endblock %}
|
||||
|
||||
</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">
|
||||
<button class='filterBuilderRemove btn btn-danger btn-sm float-right'><i class="fas fa-trash-alt"></i></button>
|
||||
</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>
|
||||
|
@ -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 %}
|
||||
<li class="breadcrumb-item">
|
||||
@ -8,38 +11,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block helpdesk_body %}
|
||||
<h2>{% trans 'Knowledgebase Category' %}:{% blocktrans with category.title as kbcat %}{{ kbcat }}{% endblocktrans %}</h2>
|
||||
|
||||
<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 %}
|
||||
{% include 'helpdesk/kb_category_base.html' %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ category.description }}</p>
|
||||
</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 %}
|
||||
|
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 load_helpdesk_settings %}
|
||||
{% load static from staticfiles %}
|
||||
{% load load_helpdesk_settings %}
|
||||
{% with request|load_helpdesk_settings as helpdesk_settings %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
||||
<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 %}{% 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]-->
|
||||
|
||||
{% include 'helpdesk/base-head.html' %}
|
||||
{% block helpdesk_head %}{% endblock %}
|
||||
|
||||
</head>
|
||||
@ -76,35 +42,8 @@
|
||||
|
||||
{% 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 -->
|
||||
<script src="{% static 'helpdesk/vendor/metisMenu/metisMenu.min.js' %}"></script>
|
||||
|
||||
<!-- Custom Theme JavaScript -->
|
||||
<script src="{% static 'helpdesk/js/sb-admin.js' %}"></script>
|
||||
|
||||
{% include 'helpdesk/base_js.html' %}
|
||||
{% block helpdesk_js %}{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
{% endwith %}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% extends "helpdesk/public_base.html" %}
|
||||
{% load i18n bootstrap4form %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block helpdesk_title %}{% trans "Create Ticket" %}{% endblock %}
|
||||
|
||||
@ -11,20 +11,13 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block helpdesk_body %}
|
||||
{% if helpdesk_settings.HELPDESK_SUBMIT_A_TICKET_PUBLIC %}
|
||||
<div class="container">
|
||||
<div class="card card-register mx-auto mt-5">
|
||||
<div class="card-header">{% trans "Submit a Ticket" %}</div>
|
||||
<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>
|
||||
<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>
|
||||
{% include 'helpdesk/public_create_ticket_base.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<h2>{% trans "Public ticket submission is disabled. Please contact the administrator for assistance." %}</h2>
|
||||
{% endif %}
|
||||
|
||||
{% 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 -->
|
||||
<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>
|
||||
<form method='post' action='./'>
|
||||
<fieldset>
|
||||
|
@ -39,6 +39,8 @@
|
||||
<th class="table-active">{% trans "Submitter E-Mail" %}</th>
|
||||
<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 %}
|
||||
<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 }}'>
|
||||
<button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-eye-slash"></i></button></a></strong>{% endif %}
|
||||
</td>
|
||||
@ -66,6 +68,12 @@
|
||||
<th class="table-active">{% trans "Total time spent" %}</th>
|
||||
<td>{{ ticket.time_spent_formated }}</td>
|
||||
</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>
|
||||
<th class="table-active">{% trans "Attachments" %}</th>
|
||||
<td colspan="3">
|
||||
|
@ -30,6 +30,116 @@
|
||||
{% block helpdesk_body %}
|
||||
{% 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-header">
|
||||
<i class="fas fa-hand-pointer"></i>
|
||||
@ -62,6 +172,7 @@
|
||||
<option id="filterBuilderSelect-Status" value="Status">{% trans "Status" %}</option>
|
||||
<option id="filterBuilderSelect-Keywords" value="Keywords">{% trans "Keywords" %}</option>
|
||||
<option id="filterBuilderSelect-Dates" value="Dates">{% trans "Date Range" %}</option>
|
||||
<option id="filterBuilderSelect-KBItems" value="KBItems">{% trans "Knowledge base items" %}</option>
|
||||
</select>
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
@ -87,6 +198,9 @@
|
||||
<li class="list-group-item filterBox{% if query_params.search_string %} filterBoxShow{% endif %}" id="filterBoxKeywords">
|
||||
{% include './filters/keywords.html' %}
|
||||
</li>
|
||||
<li class="list-group-item filterBox{% if query_params.filtering.kbitem__in %} filterBoxShow{% endif %}" id="filterBoxKBItems">
|
||||
{% include './filters/kbitems.html' %}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<input class="btn btn-primary btn-sm" type='submit' value='{% trans "Apply Filters" %}' />
|
||||
@ -162,64 +276,6 @@
|
||||
</div>
|
||||
<!-- 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 %}
|
||||
|
||||
|
||||
@ -303,6 +359,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{"data": "submitter"},
|
||||
{"data": "time_spent"},
|
||||
]
|
||||
});
|
||||
@ -345,6 +402,9 @@
|
||||
{% if query_params.search_string %}
|
||||
$("#filterBuilderSelect-Keywords")[0].disabled = "disabled";
|
||||
{% endif %}
|
||||
{% if query_params.filtering.kbitem__in %}
|
||||
$("#filterBuilderSelect-KBItems")[0].disabled = "disabled";
|
||||
{% endif %}
|
||||
});
|
||||
|
||||
{% 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',
|
||||
}
|
||||
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)
|
||||
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'])
|
||||
|
||||
# TODO: FIXME: what's wrong with this test that we get integrity errors?
|
||||
# @mock.patch('helpdesk.lib.FollowUpAttachment', autospec=True)
|
||||
# def test_autofill(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
|
||||
# """ check utf-8 data is parsed correctly """
|
||||
# self.follow_up.pk = 100
|
||||
# self.follow_up.save()
|
||||
# obj = models.FollowUpAttachment.objects.create(
|
||||
# followup=self.follow_up,
|
||||
# file=self.test_file
|
||||
# )
|
||||
# self.assertEqual(obj.filename, self.file_attrs['filename'])
|
||||
# self.assertEqual(obj.size, len(self.file_attrs['content']))
|
||||
# self.assertEqual(obj.mime_type, "text/plain")
|
||||
@mock.patch('helpdesk.lib.FollowUpAttachment', autospec=True)
|
||||
def test_autofill(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
|
||||
""" check utf-8 data is parsed correctly """
|
||||
obj = models.FollowUpAttachment.objects.create(
|
||||
followup=self.follow_up,
|
||||
file=self.test_file
|
||||
)
|
||||
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):
|
||||
""" 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 import settings
|
||||
from helpdesk.query import get_query
|
||||
from helpdesk.query import __Query__
|
||||
from helpdesk.user import HelpdeskUser
|
||||
|
||||
|
||||
@ -166,7 +166,7 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
for identifier in self.IDENTIFIERS:
|
||||
self.client.login(username='User_%d' % identifier, password=str(identifier))
|
||||
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(
|
||||
len(tickets),
|
||||
identifier * 2,
|
||||
@ -186,7 +186,7 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
# Superuser
|
||||
self.client.login(username='superuser', password='superuser')
|
||||
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(
|
||||
len(tickets),
|
||||
6,
|
||||
|
@ -2,7 +2,7 @@
|
||||
import email
|
||||
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.core import mail
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
@ -11,6 +11,7 @@ from django.test.client import Client
|
||||
from django.urls import reverse
|
||||
|
||||
from helpdesk.email import object_from_message, create_ticket_cc
|
||||
from helpdesk.tests.helpers import print_response
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@ -139,6 +140,37 @@ class TicketBasicsTestCase(TestCase):
|
||||
# Ensure only two e-mails were sent - submitter & updated.
|
||||
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):
|
||||
fixtures = ['emailtemplate.json']
|
||||
@ -976,3 +1008,24 @@ class EmailInteractionsTestCase(TestCase):
|
||||
# public_update_queue: +1
|
||||
expected_email_count += 1 + 2 + 1
|
||||
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),
|
||||
staff.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 += [
|
||||
@ -162,6 +167,14 @@ urlpatterns += [
|
||||
public.create_ticket,
|
||||
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/$',
|
||||
public.view_ticket,
|
||||
name='public_view'),
|
||||
@ -223,17 +236,17 @@ if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
kb.index,
|
||||
name='kb_index'),
|
||||
|
||||
url(r'^kb/(?P<item>[0-9]+)/$',
|
||||
kb.item,
|
||||
name='kb_item'),
|
||||
url(r'^kb/(?P<slug>[A-Za-z0-9_-]+)/$',
|
||||
kb.category,
|
||||
name='kb_category'),
|
||||
|
||||
url(r'^kb/(?P<item>[0-9]+)/vote/$',
|
||||
kb.vote,
|
||||
name='kb_vote'),
|
||||
|
||||
url(r'^kb/(?P<slug>[A-Za-z0-9_-]+)/$',
|
||||
kb.category,
|
||||
name='kb_category'),
|
||||
url(r'^kb_iframe/(?P<slug>[A-Za-z0-9_-]+)/$',
|
||||
kb.category_iframe,
|
||||
name='kb_category_iframe'),
|
||||
]
|
||||
|
||||
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.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.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)
|
||||
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,
|
||||
'items': items,
|
||||
'selected_item': selected_item,
|
||||
'query_param_string': qparams.urlencode(),
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
'iframe': iframe,
|
||||
'staff': staff,
|
||||
})
|
||||
|
||||
|
||||
def item(request, item):
|
||||
item = get_object_or_404(KBItem, pk=item)
|
||||
return render(request, 'helpdesk/kb_item.html', {
|
||||
'category': item.category,
|
||||
'item': item,
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
})
|
||||
@xframe_options_exempt
|
||||
def category_iframe(request, slug):
|
||||
return category(request, slug, iframe=True)
|
||||
|
||||
|
||||
def vote(request, item):
|
||||
item = get_object_or_404(KBItem, pk=item)
|
||||
vote = request.GET.get('vote', None)
|
||||
if vote in ('up', 'down'):
|
||||
if request.user not in item.voted_by:
|
||||
|
||||
item.votes += 1
|
||||
if vote == 'up':
|
||||
if not item.voted_by.filter(pk=request.user.pk):
|
||||
item.votes += 1
|
||||
item.voted_by.add(request.user.pk)
|
||||
item.recommendations += 1
|
||||
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())
|
||||
|
@ -7,26 +7,24 @@ views/public.py - All public facing views, eg non-staff (no authentication
|
||||
required) views.
|
||||
"""
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
try:
|
||||
# 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.shortcuts import render
|
||||
from django.utils.http import urlquote
|
||||
from django.utils.translation import ugettext as _
|
||||
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.edit import FormView
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.decorators import protect_view, is_helpdesk_staff
|
||||
import helpdesk.views.staff as staff
|
||||
import helpdesk.views.abstract_views as abstract_views
|
||||
from helpdesk.forms import PublicTicketForm
|
||||
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):
|
||||
@ -36,8 +34,7 @@ def create_ticket(request, *args, **kwargs):
|
||||
return CreateTicketView.as_view()(request, *args, **kwargs)
|
||||
|
||||
|
||||
class CreateTicketView(FormView):
|
||||
template_name = 'helpdesk/public_create_ticket.html'
|
||||
class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
|
||||
form_class = PublicTicketForm
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
@ -57,42 +54,29 @@ class CreateTicketView(FormView):
|
||||
return HttpResponseRedirect(reverse('helpdesk:dashboard'))
|
||||
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):
|
||||
request = self.request
|
||||
initial_data = {}
|
||||
try:
|
||||
queue = Queue.objects.get(slug=request.GET.get('queue', None))
|
||||
except Queue.DoesNotExist:
|
||||
queue = None
|
||||
initial_data = super().get_initial()
|
||||
|
||||
# add pre-defined data for public ticket
|
||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE'):
|
||||
# get the requested queue; return an error if queue not found
|
||||
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:
|
||||
return HttpResponse(status=500)
|
||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'):
|
||||
initial_data['priority'] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY
|
||||
if hasattr(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
|
||||
|
||||
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):
|
||||
request = self.request
|
||||
if text_is_spam(form.cleaned_data['body'], request):
|
||||
@ -115,9 +99,39 @@ class CreateTicketView(FormView):
|
||||
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):
|
||||
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):
|
||||
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 helpdesk.query import (
|
||||
get_query_class,
|
||||
query_to_dict,
|
||||
get_query,
|
||||
apply_query,
|
||||
query_tickets_by_args,
|
||||
query_to_base64,
|
||||
query_from_base64,
|
||||
)
|
||||
|
||||
from helpdesk.user import HelpdeskUser
|
||||
|
||||
from helpdesk.serializers import DatatablesTicketSerializer
|
||||
|
||||
from helpdesk.decorators import (
|
||||
helpdesk_staff_member_required, helpdesk_superuser_required,
|
||||
is_helpdesk_staff
|
||||
@ -56,9 +52,10 @@ from helpdesk.lib import (
|
||||
)
|
||||
from helpdesk.models import (
|
||||
Ticket, Queue, FollowUp, TicketChange, PreSetReply, FollowUpAttachment, SavedSearch,
|
||||
IgnoreEmail, TicketCC, TicketDependency, UserSettings,
|
||||
IgnoreEmail, TicketCC, TicketDependency, UserSettings, KBItem,
|
||||
)
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
import helpdesk.views.abstract_views as abstract_views
|
||||
from helpdesk.views.permissions import MustBeStaffMixin
|
||||
from ..lib import format_time_spent
|
||||
|
||||
@ -71,6 +68,7 @@ import re
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
Query = get_query_class()
|
||||
|
||||
if helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE:
|
||||
# treat 'normal' users like 'staff'
|
||||
@ -805,7 +803,9 @@ def ticket_list(request):
|
||||
'search_string': '',
|
||||
}
|
||||
default_query_params = {
|
||||
'filtering': {'status__in': [1, 2, 3]},
|
||||
'filtering': {
|
||||
'status__in': [1, 2, 3],
|
||||
},
|
||||
'sorting': 'created',
|
||||
'search_string': '',
|
||||
'sortreverse': False,
|
||||
@ -852,7 +852,7 @@ def ticket_list(request):
|
||||
|
||||
if saved_query:
|
||||
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
|
||||
all_queues = Queue.objects.all()
|
||||
query_params = deepcopy(default_query_params)
|
||||
@ -861,6 +861,7 @@ def ticket_list(request):
|
||||
('queue', 'queue__id__in'),
|
||||
('assigned_to', 'assigned_to__id__in'),
|
||||
('status', 'status__in'),
|
||||
('kbitem', 'kbitem__in'),
|
||||
]
|
||||
|
||||
for param, filter_command in filter_in_params:
|
||||
@ -887,7 +888,7 @@ def ticket_list(request):
|
||||
|
||||
# SORTING
|
||||
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'
|
||||
query_params['sorting'] = sort
|
||||
|
||||
@ -896,7 +897,7 @@ def ticket_list(request):
|
||||
|
||||
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))
|
||||
|
||||
@ -910,12 +911,15 @@ def ticket_list(request):
|
||||
'<a href="http://docs.djangoproject.com/en/dev/ref/databases/#sqlite-string-matching">'
|
||||
'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(
|
||||
context,
|
||||
default_tickets_per_page=request.user.usersettings_helpdesk.tickets_per_page,
|
||||
user_choices=User.objects.filter(is_active=True, is_staff=True),
|
||||
queue_choices=huser.get_queues(),
|
||||
status_choices=Ticket.STATUS_CHOICES,
|
||||
kbitem_choices=kbitem_choices,
|
||||
urlsafe_query=urlsafe_query,
|
||||
user_saved_queries=user_saved_queries,
|
||||
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
|
||||
serializers.py. The serializers and this view use django-rest_framework methods
|
||||
"""
|
||||
objects = get_query(query, HelpdeskUser(request.user))
|
||||
model_object = query_tickets_by_args(objects, '-date_created', **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']
|
||||
query = Query(HelpdeskUser(request.user), base64query=query)
|
||||
result = query.get_datatables_context(**request.query_params)
|
||||
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
|
||||
def edit_ticket(request, 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)
|
||||
|
||||
|
||||
class CreateTicketView(MustBeStaffMixin, FormView):
|
||||
class CreateTicketView(MustBeStaffMixin, abstract_views.AbstractCreateTicketMixin, FormView):
|
||||
template_name = 'helpdesk/create_ticket.html'
|
||||
form_class = TicketForm
|
||||
|
||||
def get_initial(self):
|
||||
initial_data = {}
|
||||
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']
|
||||
initial_data = super().get_initial()
|
||||
return initial_data
|
||||
|
||||
def get_form_kwargs(self):
|
||||
|
Loading…
Reference in New Issue
Block a user