Merge pull request #797 from auto-mat/iframe_submit

Iframe submit, redesigned KB, various bug fixes
This commit is contained in:
Garret Wassermann 2020-01-20 15:34:20 -05:00 committed by GitHub
commit 6e5e8cb21f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 2341 additions and 1557 deletions

View File

@ -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.

View File

@ -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',)

View File

@ -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):
"""

File diff suppressed because it is too large Load Diff

View 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'),
),
]

View File

@ -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)

View File

@ -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,60 +51,31 @@ def query_to_dict(results, descriptions):
return output
def apply_query(queryset, params):
"""
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 params['filtering'].keys():
filter = {key: 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)
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)
)
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
return filter
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)
return ticket_qs
ORDER_COLUMN_CHOICES = Choices(
DATATABLES_ORDER_COLUMN_CHOICES = Choices(
('0', 'id'),
('2', 'priority'),
('3', 'title'),
@ -112,45 +87,135 @@ ORDER_COLUMN_CHOICES = Choices(
)
def query_tickets_by_args(objects, order_by, **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.
"""
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]
def get_query_class():
from django.conf import settings
order_column = ORDER_COLUMN_CHOICES[order_column]
# django orm '-' -> desc
if order == 'desc':
order_column = '-' + order_column
def _get_query_class():
return __Query__
return getattr(settings,
'HELPDESK_QUERY_CLASS',
_get_query_class)()
queryset = objects.all().order_by(order_by)
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))
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
count = queryset.count()
queryset = queryset.order_by(order_column)[start:start + length]
return {
'items': queryset,
'count': count,
'total': total,
'draw': draw
}
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.
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,
}

View File

@ -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)

View 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 %}

View File

@ -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>
@ -79,10 +17,10 @@
<body id="bg-dark">
{% include "helpdesk/navigation-header.html" %}
<div id="wrapper">
{% include "helpdesk/navigation-sidebar.html" %}
<div id="content-wrapper">
<div class="container-fluid">
@ -105,35 +43,9 @@
<!-- /#wrapper -->
{% 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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 %}
</div>
<div class="card-body">
<p>{{ category.description }}</p>
</div>
{% include 'helpdesk/kb_category_base.html' %}
</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 %}

View 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 %}

View 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>

View File

@ -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 %}

View File

@ -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>
@ -49,62 +15,35 @@
<body id="bg-dark">
{% include "helpdesk/navigation-header.html" %}
<div id="wrapper">
{% include "helpdesk/navigation-sidebar.html" %}
<div id="content-wrapper">
<div class="container-fluid">
<!-- Breadcrumbs-->
<ol class="breadcrumb">
{% block helpdesk_breadcrumb %}{% endblock %}
</ol>
{% block helpdesk_body %}{% endblock %}
</div>
<!-- /.container-fluid -->
{% include "helpdesk/attribution.html" %}
</div>
<!-- /.content-wrapper -->
</div>
<!-- /#wrapper -->
{% 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 %}

View File

@ -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="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>&nbsp;{% 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 %}
</div>
{% endblock %}

View 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>&nbsp;{% trans "Submit Ticket" %}</button>
{% csrf_token %}</form>
{% else %}
<h2>{% trans "Public ticket submission is disabled. Please contact the administrator for assistance." %}</h2>
{% endif %}
{% endwith %}

View 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>

View 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>

View File

@ -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>

View File

@ -37,8 +37,10 @@
</strong>{% endifequal %}
</td>
<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 %}
<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">

View File

@ -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>&nbsp;</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>&nbsp;{% trans "All" %}
</button>
<button id='select_none_btn' type="button" class="btn btn-primary btn-sm"><i class="fas fa-times-circle"></i>&nbsp;{% trans "None" %}</button>
<button id='select_inverse_btn' type="button" class="btn btn-primary btn-sm"><i class="fas fa-expand-arrows-alt"></i>&nbsp;{% 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>&nbsp;{% 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>&nbsp;</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>&nbsp;{% trans "All" %}
</button>
<button id='select_none_btn' type="button" class="btn btn-primary btn-sm"><i class="fas fa-times-circle"></i>&nbsp;{% trans "None" %}</button>
<button id='select_inverse_btn' type="button" class="btn btn-primary btn-sm"><i class="fas fa-expand-arrows-alt"></i>&nbsp;{% 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>&nbsp;{% trans "Go" %}</button>
</p>
{% csrf_token %}</form>
</div>
<!-- /.panel-body -->
</div>
<!-- /.panel -->
{% endblock %}
@ -267,8 +323,8 @@
var name = data.split(" ")[1];
if (type === 'display')
{
data = '<div class="tickettitle"><a href="' + get_url(row) + '" >' +
row.id + '. ' +
data = '<div class="tickettitle"><a href="' + get_url(row) + '" >' +
row.id + '. ' +
row.title + '</a></div>';
}
return data
@ -297,12 +353,13 @@
"render": function(data, type, row, meta) {
if (data != "None") {
return data;
}
}
else {
return "";
}
}
},
{"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 %}

View File

@ -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>

View File

@ -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
View 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&amp;title=lol")

View File

@ -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,

View File

@ -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">')

View File

@ -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 += [

View 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

View File

@ -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:
if vote == 'up':
if not item.voted_by.filter(pk=request.user.pk):
item.votes += 1
if vote == 'up':
item.recommendations += 1
item.save()
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())

View File

@ -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.urls 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:

View File

@ -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):