Associate tickets with KB items

This commit is contained in:
Timothy Hobbs 2020-01-08 18:39:41 +01:00
parent 7fe6444f8f
commit 6579ac0e6f
10 changed files with 118 additions and 61 deletions

View File

@ -177,6 +177,17 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
help_text=_('You can attach a file such as a document or screenshot to this ticket.'), help_text=_('You can attach a file such as a document or screenshot to this ticket.'),
) )
def __init__(self, kbcategory=None, *args, **kwargs):
super().__init__(*args, **kwargs)
if kbcategory:
self.fields['kbitem'] = forms.ChoiceField(
widget=forms.Select(attrs={'class': 'form-control'}),
required=False,
label=_('Knowedge Base Item'),
choices=[(kbi.pk, kbi.title) for kbi in KBItem.objects.filter(category=kbcategory.pk)],
)
def _add_form_custom_fields(self, staff_only_filter=None): def _add_form_custom_fields(self, staff_only_filter=None):
if staff_only_filter is None: if staff_only_filter is None:
queryset = CustomField.objects.all() queryset = CustomField.objects.all()
@ -184,11 +195,8 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
queryset = CustomField.objects.filter(staff_only=staff_only_filter) queryset = CustomField.objects.filter(staff_only=staff_only_filter)
for field in queryset: for field in queryset:
instanceargs = { instanceargs = { 'label': field.label, 'help_text':
'label': field.label, field.help_text, 'required': field.required, }
'help_text': field.help_text,
'required': field.required,
}
self.customfield_to_field(field, instanceargs) self.customfield_to_field(field, instanceargs)
@ -341,22 +349,13 @@ class PublicTicketForm(AbstractTicketForm):
help_text=_('We will e-mail you when your ticket is updated.'), help_text=_('We will e-mail you when your ticket is updated.'),
) )
def __init__(self, hidden_fields=(), readonly_fields=(), kbcategory=None, *args, **kwargs): def __init__(self, hidden_fields=(), readonly_fields=(), *args, **kwargs):
""" """
Add any (non-staff) custom fields that are defined to the form Add any (non-staff) custom fields that are defined to the form
""" """
super(PublicTicketForm, self).__init__(*args, **kwargs) super(PublicTicketForm, self).__init__(*args, **kwargs)
self._add_form_custom_fields(False) self._add_form_custom_fields(False)
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)],
)
field_hide_table = { field_hide_table = {
'queue': 'HELPDESK_PUBLIC_TICKET_QUEUE', 'queue': 'HELPDESK_PUBLIC_TICKET_QUEUE',
'priority': 'HELPDESK_PUBLIC_TICKET_PRIORITY', 'priority': 'HELPDESK_PUBLIC_TICKET_PRIORITY',

View File

@ -1320,6 +1320,11 @@ class KBItem(models.Model):
from django.urls import reverse from django.urls import reverse
return str(reverse('helpdesk:kb_category', args=(self.category.slug,)))+"?kbitem="+str(self.pk) 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 get_markdown(self): def get_markdown(self):
return get_markdown(self.answer) return get_markdown(self.answer)

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

@ -43,6 +43,7 @@
</div> </div>
{% blocktrans with recommendations=item.recommendations votes=item.votes %}{{ recommendations }} people found this answer useful of {{votes}}. {% endblocktrans %} {% blocktrans with recommendations=item.recommendations votes=item.votes %}{{ recommendations }} people found this answer useful of {{votes}}. {% endblocktrans %}
{% endif %} {% endif %}
<a href='{% url 'helpdesk:submit' %}?{% if category.queue %}queue={{category.queue.pk}};_readonly_fields_=queue;{%endif%}kbitem={{item.id}};{{query_param_string}}'><button type="button" class="btn btn-success btn-circle btn-xl float-right"><i class="fa fa-envelope fa-lg"></i> {% trans 'Get help with this topic' %}</button></a>
</div> </div>
</div> </div>
@ -51,7 +52,7 @@
{% endfor %} {% endfor %}
</div> </div>
{% if category.queue %} {% if category.queue %}
<a href='{% url 'helpdesk:submit' %}?queue={{category.queue.pk}};_readonly_fields_=queue'><button type="button" class="btn btn-success btn-circle btn-xl float-right"><i class="fa fa-envelope fa-lg"></i> {% trans 'Get help with this topic' %}</button></a> <a href='{% url 'helpdesk:submit' %}?queue={{category.queue.pk}};_readonly_fields_=queue;{{query_param_string}}'><button type="button" class="btn btn-success btn-circle btn-xl float-right"><i class="fa fa-envelope fa-lg"></i> {% trans 'Get help with this topic' %}</button></a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -68,6 +68,12 @@
<th class="table-active">{% trans "Total time spent" %}</th> <th class="table-active">{% trans "Total time spent" %}</th>
<td>{{ ticket.time_spent_formated }}</td> <td>{{ ticket.time_spent_formated }}</td>
</tr> </tr>
{% if ticket.kbitem %}
<tr>
<th class="table-active">{% trans "Knowlegebase item" %}</th>
<td> <a href ="{{ticket.kbitem.query_url}}"> {{ticket.kbitem.title}} </a> </td>
</tr>
{% endif %}
<tr> <tr>
<th class="table-active">{% trans "Attachments" %}</th> <th class="table-active">{% trans "Attachments" %}</th>
<td colspan="3"> <td colspan="3">

View File

@ -171,6 +171,7 @@
<option id="filterBuilderSelect-Status" value="Status">{% trans "Status" %}</option> <option id="filterBuilderSelect-Status" value="Status">{% trans "Status" %}</option>
<option id="filterBuilderSelect-Keywords" value="Keywords">{% trans "Keywords" %}</option> <option id="filterBuilderSelect-Keywords" value="Keywords">{% trans "Keywords" %}</option>
<option id="filterBuilderSelect-Dates" value="Dates">{% trans "Date Range" %}</option> <option id="filterBuilderSelect-Dates" value="Dates">{% trans "Date Range" %}</option>
<option id="filterBuilderSelect-KBItems" value="KBItems">{% trans "Knowledge base items" %}</option>
</select> </select>
{% csrf_token %} {% csrf_token %}
</form> </form>
@ -196,6 +197,9 @@
<li class="list-group-item filterBox{% if query_params.search_string %} filterBoxShow{% endif %}" id="filterBoxKeywords"> <li class="list-group-item filterBox{% if query_params.search_string %} filterBoxShow{% endif %}" id="filterBoxKeywords">
{% include './filters/keywords.html' %} {% include './filters/keywords.html' %}
</li> </li>
<li class="list-group-item filterBox{% if query_params.filtering.kbitem__in %} filterBoxShow{% endif %}" id="filterBoxKBItems">
{% include './filters/kbitems.html' %}
</li>
</ul> </ul>
<input class="btn btn-primary btn-sm" type='submit' value='{% trans "Apply Filters" %}' /> <input class="btn btn-primary btn-sm" type='submit' value='{% trans "Apply Filters" %}' />
@ -396,6 +400,9 @@
{% if query_params.search_string %} {% if query_params.search_string %}
$("#filterBuilderSelect-Keywords")[0].disabled = "disabled"; $("#filterBuilderSelect-Keywords")[0].disabled = "disabled";
{% endif %} {% endif %}
{% if query_params.filtering.kbitem__in %}
$("#filterBuilderSelect-KBItems")[0].disabled = "disabled";
{% endif %}
}); });
{% for f in query_params.filtering %} {% for f in query_params.filtering %}

View File

@ -0,0 +1,37 @@
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

@ -30,12 +30,18 @@ def category(request, slug):
selected_item = request.GET.get('kbitem', None) selected_item = request.GET.get('kbitem', None)
try: try:
selected_item = int(selected_item) selected_item = int(selected_item)
except ValueError: except TypeError:
pass
qparams = request.GET.copy()
try:
del qparams['kbitem']
except KeyError:
pass pass
return render(request, 'helpdesk/kb_category.html', { return render(request, 'helpdesk/kb_category.html', {
'category': category, 'category': category,
'items': items, 'items': items,
'selected_item': selected_item, 'selected_item': selected_item,
'query_param_string': qparams.urlencode(),
'helpdesk_settings': helpdesk_settings, 'helpdesk_settings': helpdesk_settings,
}) })

View File

@ -19,6 +19,7 @@ from django.views.generic.edit import FormView
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.decorators import protect_view, is_helpdesk_staff from helpdesk.decorators import protect_view, is_helpdesk_staff
import helpdesk.views.staff as staff import helpdesk.views.staff as staff
import helpdesk.views.abstract_views as abstract_views
from helpdesk.forms import PublicTicketForm from helpdesk.forms import PublicTicketForm
from helpdesk.lib import text_is_spam from helpdesk.lib import text_is_spam
from helpdesk.models import CustomField, Ticket, Queue, UserSettings, KBCategory, KBItem from helpdesk.models import CustomField, Ticket, Queue, UserSettings, KBCategory, KBItem
@ -31,7 +32,7 @@ def create_ticket(request, *args, **kwargs):
return CreateTicketView.as_view()(request, *args, **kwargs) return CreateTicketView.as_view()(request, *args, **kwargs)
class BaseCreateTicketView(FormView): class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
form_class = PublicTicketForm form_class = PublicTicketForm
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
@ -51,54 +52,27 @@ class BaseCreateTicketView(FormView):
return HttpResponseRedirect(reverse('helpdesk:dashboard')) return HttpResponseRedirect(reverse('helpdesk:dashboard'))
return super().dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['kb_categories'] = KBCategory.objects.all()
return context
def get_initial(self): def get_initial(self):
request = self.request request = self.request
initial_data = {} initial_data = super().get_initial()
try:
queue = Queue.objects.get(slug=request.GET.get('queue', None))
except Queue.DoesNotExist:
queue = None
# add pre-defined data for public ticket # add pre-defined data for public ticket
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE'): if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE'):
# get the requested queue; return an error if queue not found # get the requested queue; return an error if queue not found
try: try:
queue = Queue.objects.get(slug=settings.HELPDESK_PUBLIC_TICKET_QUEUE) initial_data['queue'] = Queue.objects.get(slug=settings.HELPDESK_PUBLIC_TICKET_QUEUE).id
except Queue.DoesNotExist: except Queue.DoesNotExist:
return HttpResponse(status=500) return HttpResponse(status=500)
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'): if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'):
initial_data['priority'] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY initial_data['priority'] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_DUE_DATE'): if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_DUE_DATE'):
initial_data['due_date'] = settings.HELPDESK_PUBLIC_TICKET_DUE_DATE initial_data['due_date'] = settings.HELPDESK_PUBLIC_TICKET_DUE_DATE
if queue:
initial_data['queue'] = queue.id
if request.user.is_authenticated and request.user.email:
initial_data['submitter_email'] = request.user.email
query_param_fields = ['submitter_email', 'title', 'body', '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 return initial_data
def get_form_kwargs(self, *args, **kwargs): def get_form_kwargs(self, *args, **kwargs):
kwargs = super().get_form_kwargs(*args, **kwargs) kwargs = super().get_form_kwargs(*args, **kwargs)
kwargs['hidden_fields'] = self.request.GET.get('_hide_fields_', '').split(',') kwargs['hidden_fields'] = self.request.GET.get('_hide_fields_', '').split(',')
kwargs['readonly_fields'] = self.request.GET.get('_readonly_fields_', '').split(',') kwargs['readonly_fields'] = self.request.GET.get('_readonly_fields_', '').split(',')
kbitem = self.request.GET.get('kbitem', None)
if kbitem:
try:
kwargs['kbcategory'] = KBItem.objects.get(pk=int(kbitem))
except (ValueError, KBItem.DoesNotExist):
pass
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
@ -134,6 +108,11 @@ class CreateTicketView(BaseCreateTicketView):
class Homepage(CreateTicketView): class Homepage(CreateTicketView):
template_name = 'helpdesk/public_homepage.html' template_name = 'helpdesk/public_homepage.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['kb_categories'] = KBCategory.objects.all()
return context
def search_for_ticket(request, error_message=None): def search_for_ticket(request, error_message=None):
if hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC: if hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC:

View File

@ -52,9 +52,10 @@ from helpdesk.lib import (
) )
from helpdesk.models import ( from helpdesk.models import (
Ticket, Queue, FollowUp, TicketChange, PreSetReply, FollowUpAttachment, SavedSearch, Ticket, Queue, FollowUp, TicketChange, PreSetReply, FollowUpAttachment, SavedSearch,
IgnoreEmail, TicketCC, TicketDependency, UserSettings, IgnoreEmail, TicketCC, TicketDependency, UserSettings, KBItem,
) )
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
import helpdesk.views.abstract_views as abstract_views
from helpdesk.views.permissions import MustBeStaffMixin from helpdesk.views.permissions import MustBeStaffMixin
from ..lib import format_time_spent from ..lib import format_time_spent
@ -802,7 +803,9 @@ def ticket_list(request):
'search_string': '', 'search_string': '',
} }
default_query_params = { default_query_params = {
'filtering': {'status__in': [1, 2, 3]}, 'filtering': {
'status__in': [1, 2, 3],
},
'sorting': 'created', 'sorting': 'created',
'search_string': '', 'search_string': '',
'sortreverse': False, 'sortreverse': False,
@ -849,7 +852,7 @@ def ticket_list(request):
if saved_query: if saved_query:
pass pass
elif not {'queue', 'assigned_to', 'status', 'q', 'sort', 'sortreverse'}.intersection(request.GET): elif not {'queue', 'assigned_to', 'status', 'q', 'sort', 'sortreverse', 'kbitem'}.intersection(request.GET):
# Fall-back if no querying is being done # Fall-back if no querying is being done
all_queues = Queue.objects.all() all_queues = Queue.objects.all()
query_params = deepcopy(default_query_params) query_params = deepcopy(default_query_params)
@ -858,6 +861,7 @@ def ticket_list(request):
('queue', 'queue__id__in'), ('queue', 'queue__id__in'),
('assigned_to', 'assigned_to__id__in'), ('assigned_to', 'assigned_to__id__in'),
('status', 'status__in'), ('status', 'status__in'),
('kbitem', 'kbitem__in'),
] ]
for param, filter_command in filter_in_params: for param, filter_command in filter_in_params:
@ -884,7 +888,7 @@ def ticket_list(request):
# SORTING # SORTING
sort = request.GET.get('sort', None) sort = request.GET.get('sort', None)
if sort not in ('status', 'assigned_to', 'created', 'title', 'queue', 'priority'): if sort not in ('status', 'assigned_to', 'created', 'title', 'queue', 'priority', 'kbitem'):
sort = 'created' sort = 'created'
query_params['sorting'] = sort query_params['sorting'] = sort
@ -907,12 +911,15 @@ def ticket_list(request):
'<a href="http://docs.djangoproject.com/en/dev/ref/databases/#sqlite-string-matching">' '<a href="http://docs.djangoproject.com/en/dev/ref/databases/#sqlite-string-matching">'
'Django Documentation on string matching in SQLite</a>.') 'Django Documentation on string matching in SQLite</a>.')
kbitem_choices = [(item.pk, item.title) for item in KBItem.objects.all()]
return render(request, 'helpdesk/ticket_list.html', dict( return render(request, 'helpdesk/ticket_list.html', dict(
context, context,
default_tickets_per_page=request.user.usersettings_helpdesk.tickets_per_page, default_tickets_per_page=request.user.usersettings_helpdesk.tickets_per_page,
user_choices=User.objects.filter(is_active=True, is_staff=True), user_choices=User.objects.filter(is_active=True, is_staff=True),
queue_choices=huser.get_queues(), queue_choices=huser.get_queues(),
status_choices=Ticket.STATUS_CHOICES, status_choices=Ticket.STATUS_CHOICES,
kbitem_choices=kbitem_choices,
urlsafe_query=urlsafe_query, urlsafe_query=urlsafe_query,
user_saved_queries=user_saved_queries, user_saved_queries=user_saved_queries,
query_params=query_params, query_params=query_params,
@ -992,17 +999,12 @@ def edit_ticket(request, ticket_id):
edit_ticket = staff_member_required(edit_ticket) edit_ticket = staff_member_required(edit_ticket)
class CreateTicketView(MustBeStaffMixin, FormView): class CreateTicketView(MustBeStaffMixin, abstract_views.AbstractCreateTicketMixin, FormView):
template_name = 'helpdesk/create_ticket.html' template_name = 'helpdesk/create_ticket.html'
form_class = TicketForm form_class = TicketForm
def get_initial(self): def get_initial(self):
initial_data = {} initial_data = super().get_initial()
request = self.request
if request.user.usersettings_helpdesk.use_email_as_submitter and request.user.email:
initial_data['submitter_email'] = request.user.email
if 'queue' in request.GET:
initial_data['queue'] = request.GET['queue']
return initial_data return initial_data
def get_form_kwargs(self): def get_form_kwargs(self):