Merge pull request #778 from auto-mat/datatables_refactor

Datatables refactor - now use Datatables always rather than only when serverside processing is disabled.
This commit is contained in:
Garret Wassermann 2019-10-11 14:40:22 -04:00 committed by GitHub
commit 406ecf5411
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 476 additions and 508 deletions

View File

@ -106,9 +106,6 @@ HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = False
LOGIN_URL = '/login/' LOGIN_URL = '/login/'
LOGIN_REDIRECT_URL = '/login/' LOGIN_REDIRECT_URL = '/login/'
# Turn off server-side processing for this local demo
HELPDESK_USE_SERVERSIDE_PROCESSING = False
# Database # Database
# - by default, we use SQLite3 for the demo, but you can also # - by default, we use SQLite3 for the demo, but you can also
# configure MySQL or PostgreSQL, see the docs for more: # configure MySQL or PostgreSQL, see the docs for more:

View File

@ -90,11 +90,6 @@ These changes are visible throughout django-helpdesk
**Default:** ``HELPDESK_ANON_ACCESS_RAISES_404 = False`` **Default:** ``HELPDESK_ANON_ACCESS_RAISES_404 = False``
- **HELPDESK_USE_SERVERSIDE_PROCESSING** If True, may improve performance by utilizing server-side processing of the full ticket list whenever performing queries on the ticket list. Set to False to restore the "classic" functionality using javascript.
**Default:** ``HELPDESK_USE_SERVERSIDE_PROCESSING = True``
Options shown on public pages Options shown on public pages
----------------------------- -----------------------------

View File

@ -11,98 +11,15 @@ import mimetypes
import os import os
from django.conf import settings from django.conf import settings
from django.db.models import Q
from django.utils.encoding import smart_text, smart_str from django.utils.encoding import smart_text, smart_str
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from helpdesk.models import FollowUpAttachment, EmailTemplate from helpdesk.models import FollowUpAttachment, EmailTemplate
from model_utils import Choices
from base64 import b64encode
from base64 import b64decode
import json
logger = logging.getLogger('helpdesk') logger = logging.getLogger('helpdesk')
def query_to_base64(query):
"""
Converts a query dict object to a base64-encoded bytes object.
"""
return b64encode(json.dumps(query).encode('UTF-8'))
def query_from_base64(b64data):
"""
Converts base64-encoded bytes object back to a query dict object.
"""
return json.loads(b64decode(b64data).decode('utf-8'))
def query_to_dict(results, descriptions):
"""
Replacement method for cursor.dictfetchall() as that method no longer
exists in psycopg2, and I'm guessing in other backends too.
Converts the results of a raw SQL query into a list of dictionaries, suitable
for use in templates etc.
"""
output = []
for data in results:
row = {}
i = 0
for column in descriptions:
row[column[0]] = data[i]
i += 1
output.append(row)
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', None)
if search:
qset = (
Q(title__icontains=search) |
Q(description__icontains=search) |
Q(resolution__icontains=search) |
Q(submitter_email__icontains=search) |
Q(ticketcustomfieldvalue__value__icontains=search)
)
queryset = queryset.filter(qset)
sorting = params.get('sorting', None)
if sorting:
sortreverse = params.get('sortreverse', None)
if sortreverse:
sorting = "-%s" % sorting
queryset = queryset.order_by(sorting)
return queryset
def ticket_template_context(ticket): def ticket_template_context(ticket):
context = {} context = {}
@ -238,62 +155,6 @@ def process_attachments(followup, attached_files):
return attachments return attachments
ORDER_COLUMN_CHOICES = Choices(
('0', 'id'),
('2', 'priority'),
('3', 'title'),
('4', 'queue'),
('5', 'status'),
('6', 'created'),
('7', 'due_date'),
('8', 'assigned_to')
)
def query_tickets_by_args(objects, order_by, **kwargs):
"""
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 TicketSerializer in serializers.py.
"""
draw = int(kwargs.get('draw', None)[0])
length = int(kwargs.get('length', None)[0])
start = int(kwargs.get('start', None)[0])
search_value = kwargs.get('search[value]', None)[0]
order_column = kwargs.get('order[0][column]', None)[0]
order = kwargs.get('order[0][dir]', None)[0]
order_column = ORDER_COLUMN_CHOICES[order_column]
# 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(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))
count = queryset.count()
queryset = queryset.order_by(order_column)[start:start + length]
return {
'items': queryset,
'count': count,
'total': total,
'draw': draw
}
def format_time_spent(time_spent): def format_time_spent(time_spent):
"""Format time_spent attribute to "[H]HHh:MMm" text string to be allign in """Format time_spent attribute to "[H]HHh:MMm" text string to be allign in
all graphical outputs all graphical outputs

View File

@ -28,6 +28,8 @@ from markdown.extensions import Extension
import uuid import uuid
from helpdesk import settings as helpdesk_settings
from .templated_email import send_templated_mail from .templated_email import send_templated_mail
@ -685,7 +687,7 @@ class Ticket(models.Model):
site = Site.objects.get_current() site = Site.objects.get_current()
except ImproperlyConfigured: except ImproperlyConfigured:
site = Site(domain='configure-django-sites.com') site = Site(domain='configure-django-sites.com')
if settings.HELPDESK_USE_HTTPS_IN_EMAIL_LINK: if helpdesk_settings.HELPDESK_USE_HTTPS_IN_EMAIL_LINK:
protocol = 'https' protocol = 'https'
else: else:
protocol = 'http' protocol = 'http'
@ -711,7 +713,7 @@ class Ticket(models.Model):
site = Site.objects.get_current() site = Site.objects.get_current()
except ImproperlyConfigured: except ImproperlyConfigured:
site = Site(domain='configure-django-sites.com') site = Site(domain='configure-django-sites.com')
if settings.HELPDESK_USE_HTTPS_IN_EMAIL_LINK: if helpdesk_settings.HELPDESK_USE_HTTPS_IN_EMAIL_LINK:
protocol = 'https' protocol = 'https'
else: else:
protocol = 'http' protocol = 'http'

156
helpdesk/query.py Normal file
View File

@ -0,0 +1,156 @@
from django.db.models import Q
from django.core.cache import cache
from model_utils import Choices
from base64 import b64encode
from base64 import b64decode
import json
def query_to_base64(query):
"""
Converts a query dict object to a base64-encoded bytes object.
"""
return b64encode(json.dumps(query).encode('UTF-8')).decode("ascii")
def query_from_base64(b64data):
"""
Converts base64-encoded bytes object back to a query dict object.
"""
query = {'search_string': ''}
query.update(json.loads(b64decode(b64data).decode('utf-8')))
if query['search_string'] is None:
query['search_string'] = ''
return query
def query_to_dict(results, descriptions):
"""
Replacement method for cursor.dictfetchall() as that method no longer
exists in psycopg2, and I'm guessing in other backends too.
Converts the results of a raw SQL query into a list of dictionaries, suitable
for use in templates etc.
"""
output = []
for data in results:
row = {}
i = 0
for column in descriptions:
row[column[0]] = data[i]
i += 1
output.append(row)
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)
)
queryset = queryset.filter(qset)
sorting = params.get('sorting', None)
if sorting:
sortreverse = params.get('sortreverse', None)
if sortreverse:
sorting = "-%s" % sorting
queryset = queryset.order_by(sorting)
return queryset
def get_query(query, huser):
# 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=60*60)
return ticket_qs
ORDER_COLUMN_CHOICES = Choices(
('0', 'id'),
('2', 'priority'),
('3', 'title'),
('4', 'queue'),
('5', 'status'),
('6', 'created'),
('7', 'due_date'),
('8', 'assigned_to')
)
def query_tickets_by_args(objects, order_by, **kwargs):
"""
This function takes in a list of ticket objects from the views and throws it
to the datatables on ticket_list.html. If a search string was entered, this
function filters existing dataset on search string and returns a filtered
filtered list. The `draw`, `length` etc parameters are for datatables to
display meta data on the table contents. The returning queryset is passed
to a Serializer called DatatablesTicketSerializer in serializers.py.
"""
draw = int(kwargs.get('draw', None)[0])
length = int(kwargs.get('length', None)[0])
start = int(kwargs.get('start', None)[0])
search_value = kwargs.get('search[value]', None)[0]
order_column = kwargs.get('order[0][column]', None)[0]
order = kwargs.get('order[0][dir]', None)[0]
order_column = ORDER_COLUMN_CHOICES[order_column]
# 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(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))
count = queryset.count()
queryset = queryset.order_by(order_column)[start:start + length]
return {
'items': queryset,
'count': count,
'total': total,
'draw': draw
}

View File

@ -12,7 +12,7 @@ datatables for ticket_list.html. Called from staff.datatables_ticket_list.
""" """
class TicketSerializer(serializers.ModelSerializer): class DatatablesTicketSerializer(serializers.ModelSerializer):
ticket = serializers.SerializerMethodField() ticket = serializers.SerializerMethodField()
assigned_to = serializers.SerializerMethodField() assigned_to = serializers.SerializerMethodField()
created = serializers.SerializerMethodField() created = serializers.SerializerMethodField()

View File

@ -151,7 +151,3 @@ HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = getattr(
# use https in the email links # use https in the email links
HELPDESK_USE_HTTPS_IN_EMAIL_LINK = getattr(settings, 'HELPDESK_USE_HTTPS_IN_EMAIL_LINK', False) HELPDESK_USE_HTTPS_IN_EMAIL_LINK = getattr(settings, 'HELPDESK_USE_HTTPS_IN_EMAIL_LINK', False)
# Asynchronous Datatables - Optional
HELPDESK_USE_SERVERSIDE_PROCESSING = getattr(
settings, 'HELPDESK_USE_SERVERSIDE_PROCESSING', True)

View File

@ -4,7 +4,7 @@
<label for='id_query'>{% trans "Keywords" %}</label> <label for='id_query'>{% trans "Keywords" %}</label>
</div> </div>
<div class="col col-sm-3"> <div class="col col-sm-3">
<input type='text' name='q' value='{{ query }}' id='id_query' /> <input type='text' name='q' value='{{ query_params.search_string }}' id='id_query' />
</div> </div>
<div class="col col-sm-6"> <div class="col col-sm-6">
<button class='filterBuilderRemove btn btn-danger btn-sm float-right'><i class="fas fa-trash-alt"></i></button> <button class='filterBuilderRemove btn btn-danger btn-sm float-right'><i class="fas fa-trash-alt"></i></button>

View File

@ -84,7 +84,7 @@
<li class="list-group-item filterBox{% if query_params.filtering.created__gte or query_params.filtering.created__lte %} filterBoxShow{% endif %}" id='filterBoxDates'> <li class="list-group-item filterBox{% if query_params.filtering.created__gte or query_params.filtering.created__lte %} filterBoxShow{% endif %}" id='filterBoxDates'>
{% include './filters/date.html' %} {% include './filters/date.html' %}
</li> </li>
<li class="list-group-item filterBox{% if query %} 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>
</ul> </ul>
@ -185,9 +185,6 @@
<th>{% trans "Time Spent" %}</th> <th>{% trans "Time Spent" %}</th>
</tr> </tr>
</thead> </thead>
{% if not server_side %}
{% include 'helpdesk/ticket_list_table.html' %}
{% endif %}
</table> </table>
<p><label>{% trans "Select:" %} </label> <p><label>{% trans "Select:" %} </label>
@ -229,135 +226,126 @@
{% block helpdesk_js %} {% block helpdesk_js %}
<script src='{% static "helpdesk/filter.js" %}'></script> <script src='{% static "helpdesk/filter.js" %}'></script>
<script> <script>
{% if not server_side %} function get_url(row)
$('#ticketTable').DataTable({ {
"language": { return "{% url 'helpdesk:view' 1234 %}".replace(/1234/, row.id.toString());
"emptyTable": "{% trans 'No Tickets Match Your Selection' %}" }
}, $(document).ready(function()
"order": [], {
responsive: true //DataTables Initialization
}); let tasks_table = $('#ticketTable').DataTable({
{% else %} "language": {
function get_url(row) "emptyTable": "{% trans 'No Tickets Match Your Selection' %}"
{ },
return "{% url 'helpdesk:view' 1234 %}".replace(/1234/, row.id.toString()); "processing": true,
} "serverSide": true,
$(document).ready(function() "ajax": {
{ "url": "{% url 'helpdesk:datatables_ticket_list' urlsafe_query %}",
//DataTables Initialization "type": "GET",
let tasks_table = $('#ticketTable').DataTable({ },
"language": { createdRow: function( row, data, dataIndex )
"emptyTable": "{% trans 'No Tickets Match Your Selection' %}" {
}, $( row ).addClass(data.row_class);
"processing": true, },
"serverSide": true,
"ajax": {
"url": "{% url 'helpdesk:datatables_ticket_list' %}",
"type": "GET",
},
createdRow: function( row, data, dataIndex )
{
$( row ).addClass(data.row_class);
},
"columns": [ "columns": [
{"data": "id", {"data": "id",
"orderable": false, "orderable": false,
"render": function(data, type, row, meta) "render": function(data, type, row, meta)
{ {
var pk = data; var pk = data;
if(type === 'display'){ if(type === 'display'){
data = "<input type='checkbox' name='ticket_id' value='"+pk+"'"+ "class='ticket_multi_select' />" data = "<input type='checkbox' name='ticket_id' value='"+pk+"'"+ "class='ticket_multi_select' />"
} }
return data return data
} }
}, },
{"data": "ticket", {"data": "ticket",
"render": function (data, type, row, meta) "render": function (data, type, row, meta)
{ {
var id = data.split(" ")[0]; var id = data.split(" ")[0];
var name = data.split(" ")[1]; var name = data.split(" ")[1];
if (type === 'display') if (type === 'display')
{ {
data = '<div class="tickettitle"><a href="' + get_url(row) + '" >' + data = '<div class="tickettitle"><a href="' + get_url(row) + '" >' +
row.id + '. ' + row.id + '. ' +
row.title + '</a></div>'; row.title + '</a></div>';
} }
return data return data
} }
}, },
{"data": "priority", {"data": "priority",
"render": function (data, type, row, meta) { "render": function (data, type, row, meta) {
var priority = "success"; var priority = "success";
if (data == 4 ) { if (data == 4 ) {
priority = "warning"; priority = "warning";
} else if (data == 5) { } else if (data == 5) {
priority = "danger"; priority = "danger";
} }
return '<p class="text-'+priority+'">'+data+'</p>'; return '<p class="text-'+priority+'">'+data+'</p>';
} }
}, },
{"data": "queue", {"data": "queue",
"render": function(data, type, row, meta) { "render": function(data, type, row, meta) {
return data.title; return data.title;
} }
}, },
{"data": "status"}, {"data": "status"},
{"data": "created"}, {"data": "created"},
{"data": "due_date"}, {"data": "due_date"},
{"data": "assigned_to", {"data": "assigned_to",
"render": function(data, type, row, meta) { "render": function(data, type, row, meta) {
if (data != "None") { if (data != "None") {
return data; return data;
} }
else { else {
return ""; return "";
} }
} }
}, },
{"data": "time_spent"}, {"data": "time_spent"},
] ]
}); });
}) })
{% endif %}
$(document).ready(function()
{
$("#select_all_btn").click(function() {
$(".ticket_multi_select").prop('checked', true);
});
$("#select_none_btn").click(function() {
$(".ticket_multi_select").prop('checked', false);
});
$("#select_inverse_btn").click(function() {
$(".ticket_multi_select").each(function() {
$(this).prop('checked', !$(this).prop('checked'));
});
});
})
/** $(document).ready(function()
* Disable active filtering options in filter select menu {
*/ $("#select_all_btn").click(function() {
$(document).ready(function() { $(".ticket_multi_select").prop('checked', true);
{% if query_params.sorting %} });
$("#filterBuilderSelect-Sort")[0].disabled = "disabled"; $("#select_none_btn").click(function() {
{% endif %} $(".ticket_multi_select").prop('checked', false);
{% if query_params.filtering.assigned_to__id__in %} });
$("#filterBuilderSelect-Owner")[0].disabled = "disabled"; $("#select_inverse_btn").click(function() {
{% endif %} $(".ticket_multi_select").each(function() {
{% if query_params.filtering.queue__id__in %} $(this).prop('checked', !$(this).prop('checked'));
$("#filterBuilderSelect-Queue")[0].disabled = "disabled"; });
{% endif %} });
{% if query_params.filtering.status__in %} })
$("#filterBuilderSelect-Status")[0].disabled = "disabled";
{% endif %} /**
{% if query_params.filtering.created__gte or query_params.filtering.created__lte %} * Disable active filtering options in filter select menu
$("#filterBuilderSelect-Dates")[0].disabled = "disabled"; */
{% endif %} $(document).ready(function() {
{% if query %} {% if query_params.sorting %}
$("#filterBuilderSelect-Keywords")[0].disabled = "disabled"; $("#filterBuilderSelect-Sort")[0].disabled = "disabled";
{% endif %} {% endif %}
}); {% if query_params.filtering.assigned_to__id__in %}
$("#filterBuilderSelect-Owner")[0].disabled = "disabled";
{% endif %}
{% if query_params.filtering.queue__id__in %}
$("#filterBuilderSelect-Queue")[0].disabled = "disabled";
{% endif %}
{% if query_params.filtering.status__in %}
$("#filterBuilderSelect-Status")[0].disabled = "disabled";
{% endif %}
{% if query_params.filtering.created__gte or query_params.filtering.created__lte %}
$("#filterBuilderSelect-Dates")[0].disabled = "disabled";
{% endif %}
{% if query_params.search_string %}
$("#filterBuilderSelect-Keywords")[0].disabled = "disabled";
{% endif %}
});
{% for f in query_params.filtering %} {% for f in query_params.filtering %}
{% endfor %} {% endfor %}

View File

@ -37,6 +37,8 @@ class DirectTemplateView(TemplateView):
app_name = 'helpdesk' app_name = 'helpdesk'
base64_pattern = r'(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$'
urlpatterns = [ urlpatterns = [
url(r'^dashboard/$', url(r'^dashboard/$',
staff.dashboard, staff.dashboard,
@ -146,7 +148,7 @@ urlpatterns = [
staff.email_ignore_del, staff.email_ignore_del,
name='email_ignore_del'), name='email_ignore_del'),
url(r'^datatables_ticket_list/$', url(r'^datatables_ticket_list/(?P<query>{})$'.format(base64_pattern),
staff.datatables_ticket_list, staff.datatables_ticket_list,
name="datatables_ticket_list"), name="datatables_ticket_list"),
] ]

60
helpdesk/user.py Normal file
View File

@ -0,0 +1,60 @@
from helpdesk.models import (
Ticket,
Queue
)
from helpdesk import settings as helpdesk_settings
class HelpdeskUser:
def __init__(self, user):
self.user = user
def get_queues(self):
"""Return the list of Queues the user can access.
:param user: The User (the class should have the has_perm method)
:return: A Python list of Queues
"""
user = self.user
all_queues = Queue.objects.all()
public_ids = [q.pk for q in
Queue.objects.filter(allow_public_submission=True)]
limit_queues_by_user = \
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION \
and not user.is_superuser
if limit_queues_by_user:
id_list = [q.pk for q in all_queues if user.has_perm(q.permission_name)]
id_list += public_ids
return all_queues.filter(pk__in=id_list)
else:
return all_queues
def get_tickets_in_queues(self):
return Ticket.objects.filter(queue__in=self.get_queues())
def can_access_queue(self, queue):
"""Check if a certain user can access a certain queue.
:param user: The User (the class should have the has_perm method)
:param queue: The django-helpdesk Queue instance
:return: True if the user has permission (either by default or explicitly), false otherwise
"""
user = self.user
if user.is_superuser or not helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION:
return True
else:
return user.has_perm(queue.permission_name)
def can_access_ticket(self, ticket):
"""Check to see if the user has permission to access
a ticket. If not then deny access."""
user = self.user
if self.can_access_queue(ticket.queue):
return True
elif user.is_superuser or user.is_staff or \
(ticket.assigned_to and user.id == ticket.assigned_to.id):
return True
else:
return False

View File

@ -6,6 +6,9 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
views/staff.py - The bulk of the application - provides most business logic and views/staff.py - The bulk of the application - provides most business logic and
renders all staff-facing views. renders all staff-facing views.
""" """
from copy import deepcopy
import json
from django import VERSION as DJANGO_VERSION from django import VERSION as DJANGO_VERSION
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -14,7 +17,7 @@ from django.contrib.contenttypes.models import ContentType
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.core.exceptions import ValidationError, PermissionDenied from django.core.exceptions import ValidationError, PermissionDenied
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponseRedirect, Http404, HttpResponse from django.http import HttpResponseRedirect, Http404, HttpResponse, JsonResponse
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.utils.dates import MONTHS_3 from django.utils.dates import MONTHS_3
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -22,11 +25,19 @@ from django.utils.html import escape
from django import forms from django import forms
from django.utils import timezone from django.utils import timezone
from django.views.generic.edit import FormView, UpdateView from django.views.generic.edit import FormView, UpdateView
# For datatables serverside
from django.core.cache import cache
from helpdesk.lib import query_tickets_by_args from helpdesk.query import (
from helpdesk.serializers import TicketSerializer 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 ( from helpdesk.decorators import (
helpdesk_staff_member_required, helpdesk_superuser_required, helpdesk_staff_member_required, helpdesk_superuser_required,
@ -38,8 +49,10 @@ from helpdesk.forms import (
) )
from helpdesk.decorators import staff_member_required, superuser_required from helpdesk.decorators import staff_member_required, superuser_required
from helpdesk.lib import ( from helpdesk.lib import (
query_to_dict, apply_query, safe_template_context, safe_template_context,
process_attachments, queue_template_context, format_time_spent process_attachments,
queue_template_context,
format_time_spent,
) )
from helpdesk.models import ( from helpdesk.models import (
Ticket, Queue, FollowUp, TicketChange, PreSetReply, FollowUpAttachment, SavedSearch, Ticket, Queue, FollowUp, TicketChange, PreSetReply, FollowUpAttachment, SavedSearch,
@ -85,44 +98,10 @@ def _get_queue_choices(queues):
return queue_choices return queue_choices
def _get_user_queues(user):
"""Return the list of Queues the user can access.
:param user: The User (the class should have the has_perm method)
:return: A Python list of Queues
"""
all_queues = Queue.objects.all()
public_ids = [q.pk for q in
Queue.objects.filter(allow_public_submission=True)]
limit_queues_by_user = \
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION \
and not user.is_superuser
if limit_queues_by_user:
id_list = [q.pk for q in all_queues if user.has_perm(q.permission_name)]
id_list += public_ids
return all_queues.filter(pk__in=id_list)
else:
return all_queues
def _has_access_to_queue(user, queue):
"""Check if a certain user can access a certain queue.
:param user: The User (the class should have the has_perm method)
:param queue: The django-helpdesk Queue instance
:return: True if the user has permission (either by default or explicitly), false otherwise
"""
if user.is_superuser or not helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION:
return True
else:
return user.has_perm(queue.permission_name)
def _is_my_ticket(user, ticket): def _is_my_ticket(user, ticket):
"""Check to see if the user has permission to access """Check to see if the user has permission to access
a ticket. If not then deny access.""" a ticket. If not then deny access."""
if _has_access_to_queue(user, ticket.queue): if (user, ticket.queue):
return True return True
elif user.is_superuser or user.is_staff or \ elif user.is_superuser or user.is_staff or \
(ticket.assigned_to and user.id == ticket.assigned_to.id): (ticket.assigned_to and user.id == ticket.assigned_to.id):
@ -152,7 +131,7 @@ def dashboard(request):
assigned_to=request.user, assigned_to=request.user,
status__in=[Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS]) status__in=[Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS])
user_queues = _get_user_queues(request.user) user_queues = HelpdeskUser(request.user).get_queues()
unassigned_tickets = active_tickets.filter( unassigned_tickets = active_tickets.filter(
assigned_to__isnull=True, assigned_to__isnull=True,
@ -178,7 +157,7 @@ def dashboard(request):
# Queue 1 10 4 # Queue 1 10 4
# Queue 2 4 12 # Queue 2 4 12
queues = _get_user_queues(request.user).values_list('id', flat=True) queues = HelpdeskUser(request.user).get_queues().values_list('id', flat=True)
from_clause = """FROM helpdesk_ticket t, from_clause = """FROM helpdesk_ticket t,
helpdesk_queue q""" helpdesk_queue q"""
@ -199,14 +178,17 @@ def dashboard(request):
dashboard = staff_member_required(dashboard) dashboard = staff_member_required(dashboard)
def ticket_perm_check(request, ticket):
huser = HelpdeskUser(request.user)
if not huser.can_access_queue(ticket.queue):
raise PermissionDenied()
if not huser.can_access_ticket(ticket):
raise PermissionDenied()
@helpdesk_staff_member_required @helpdesk_staff_member_required
def delete_ticket(request, ticket_id): def delete_ticket(request, ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if not _has_access_to_queue(request.user, ticket.queue): ticket_perm_check(request, ticket)
raise PermissionDenied()
if not _is_my_ticket(request.user, ticket):
raise PermissionDenied()
if request.method == 'GET': if request.method == 'GET':
return render(request, 'helpdesk/delete_ticket.html', { return render(request, 'helpdesk/delete_ticket.html', {
@ -225,10 +207,7 @@ def followup_edit(request, ticket_id, followup_id):
"""Edit followup options with an ability to change the ticket.""" """Edit followup options with an ability to change the ticket."""
followup = get_object_or_404(FollowUp, id=followup_id) followup = get_object_or_404(FollowUp, id=followup_id)
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if not _has_access_to_queue(request.user, ticket.queue): ticket_perm_check(request, ticket)
raise PermissionDenied()
if not _is_my_ticket(request.user, ticket):
raise PermissionDenied()
if request.method == 'GET': if request.method == 'GET':
form = EditFollowUpForm(initial={ form = EditFollowUpForm(initial={
@ -300,10 +279,7 @@ followup_delete = staff_member_required(followup_delete)
@helpdesk_staff_member_required @helpdesk_staff_member_required
def view_ticket(request, ticket_id): def view_ticket(request, ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if not _has_access_to_queue(request.user, ticket.queue): ticket_perm_check(request, ticket)
raise PermissionDenied()
if not _is_my_ticket(request.user, ticket):
raise PermissionDenied()
if 'take' in request.GET: if 'take' in request.GET:
# Allow the user to assign the ticket to themselves whilst viewing it. # Allow the user to assign the ticket to themselves whilst viewing it.
@ -349,7 +325,7 @@ def view_ticket(request, ticket_id):
else: else:
users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD) users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD)
queues = _get_user_queues(request.user) queues = HelpdeskUser(request.user).get_queues()
queue_choices = _get_queue_choices(queues) queue_choices = _get_queue_choices(queues)
# TODO: shouldn't this template get a form to begin with? # TODO: shouldn't this template get a form to begin with?
form = TicketForm(initial={'due_date': ticket.due_date}, form = TicketForm(initial={'due_date': ticket.due_date},
@ -746,8 +722,9 @@ def mass_update(request):
user = request.user user = request.user
action = 'assign' action = 'assign'
huser = HelpdeskUser(request.user)
for t in Ticket.objects.filter(id__in=tickets): for t in Ticket.objects.filter(id__in=tickets):
if not _has_access_to_queue(request.user, t.queue): if not huser.can_access_queue(t.queue):
continue continue
if action == 'assign' and t.assigned_to != user: if action == 'assign' and t.assigned_to != user:
@ -827,9 +804,7 @@ mass_update = staff_member_required(mass_update)
def ticket_list(request): def ticket_list(request):
context = {} context = {}
user_queues = _get_user_queues(request.user) huser = HelpdeskUser(request.user)
# Prefilter the allowed tickets
base_tickets = Ticket.objects.filter(queue__in=user_queues)
# Query_params will hold a dictionary of parameters relating to # Query_params will hold a dictionary of parameters relating to
# a query, to be saved if needed: # a query, to be saved if needed:
@ -837,11 +812,14 @@ def ticket_list(request):
'filtering': {}, 'filtering': {},
'sorting': None, 'sorting': None,
'sortreverse': False, 'sortreverse': False,
'keyword': None, 'search_string': '',
'search_string': None, }
default_query_params = {
'filtering': {'status__in': [1, 2, 3]},
'sorting': 'created',
'search_string': '',
'sortreverse': False,
} }
from_saved_query = False
# If the user is coming from the header/navigation search box, lets' first # If the user is coming from the header/navigation search box, lets' first
# look at their query to see if they have entered a valid ticket number. If # look at their query to see if they have entered a valid ticket number. If
@ -871,73 +849,38 @@ def ticket_list(request):
if filter: if filter:
try: try:
ticket = base_tickets.get(**filter) ticket = huser.get_tickets_in_queues.get(**filter)
return HttpResponseRedirect(ticket.staff_url) return HttpResponseRedirect(ticket.staff_url)
except Ticket.DoesNotExist: except Ticket.DoesNotExist:
# Go on to standard keyword searching # Go on to standard keyword searching
pass pass
saved_query = None try:
if request.GET.get('saved_query', None): saved_query, query_params = load_saved_query(request, query_params)
from_saved_query = True except QueryLoadError:
try: return HttpResponseRedirect(reverse('helpdesk:list'))
saved_query = SavedSearch.objects.get(pk=request.GET.get('saved_query'))
except SavedSearch.DoesNotExist:
return HttpResponseRedirect(reverse('helpdesk:list'))
if not (saved_query.shared or saved_query.user == request.user):
return HttpResponseRedirect(reverse('helpdesk:list'))
import json
from helpdesk.lib import query_from_base64
try:
# we get a string like: b'stuff'
# so leave of the first two chars (b') and last (')
b64query = saved_query.query[2:-1]
query_params = query_from_base64(b64query)
except ValueError:
# Query deserialization failed. (E.g. was a pickled query)
return HttpResponseRedirect(reverse('helpdesk:list'))
elif not ('queue' in request.GET or
'assigned_to' in request.GET or
'status' in request.GET or
'q' in request.GET or
'sort' in request.GET or
'sortreverse' in request.GET):
# Fall-back if no querying is being done, force the list to only
# show open/reopened/resolved (not closed) cases sorted by creation
# date.
if saved_query:
pass
elif not {'queue', 'assigned_to', 'status', 'q', 'sort', 'sortreverse'}.intersection(request.GET):
# Fall-back if no querying is being done
all_queues = Queue.objects.all() all_queues = Queue.objects.all()
query_params = { query_params = deepcopy(default_query_params)
'filtering': {'status__in': [1, 2, 3]},
'sorting': 'created',
}
else: else:
queues = request.GET.getlist('queue') filter_in_params = [
if queues: ('queue', 'queue__id__in'),
try: ('assigned_to', 'assigned_to__id__in'),
queues = [int(q) for q in queues] ('status', 'status__in'),
query_params['filtering']['queue__id__in'] = queues ]
except ValueError:
pass
owners = request.GET.getlist('assigned_to') for param, filter_command in filter_in_params:
if owners: patterns = request.GET.getlist(param)
try: if patterns:
owners = [int(u) for u in owners] try:
query_params['filtering']['assigned_to__id__in'] = owners pattern_pks = [int(pattern) for pattern in patterns]
except ValueError: query_params['filtering'][filter_command] = pattern_pks
pass except ValueError:
pass
statuses = request.GET.getlist('status')
if statuses:
try:
statuses = [int(s) for s in statuses]
query_params['filtering']['status__in'] = statuses
except ValueError:
pass
date_from = request.GET.get('date_from') date_from = request.GET.get('date_from')
if date_from: if date_from:
@ -948,11 +891,9 @@ def ticket_list(request):
query_params['filtering']['created__lte'] = date_to query_params['filtering']['created__lte'] = date_to
# KEYWORD SEARCHING # KEYWORD SEARCHING
q = request.GET.get('q', None) q = request.GET.get('q', '')
context['query'] = q
if q: query_params['search_string'] = q
context = dict(context, query=q)
query_params['search_string'] = q
# SORTING # SORTING
sort = request.GET.get('sort', None) sort = request.GET.get('sort', None)
@ -963,20 +904,14 @@ def ticket_list(request):
sortreverse = request.GET.get('sortreverse', None) sortreverse = request.GET.get('sortreverse', None)
query_params['sortreverse'] = sortreverse query_params['sortreverse'] = sortreverse
tickets = base_tickets.select_related() urlsafe_query = query_to_base64(query_params)
try: get_query(urlsafe_query, huser)
ticket_qs = apply_query(tickets, query_params)
except ValidationError: user_saved_queries = SavedSearch.objects.filter(Q(user=request.user) | Q(shared__exact=True))
# invalid parameters in query, return default query
query_params = {
'filtering': {'status__in': [1, 2, 3]},
'sorting': 'created',
}
ticket_qs = apply_query(tickets, query_params)
search_message = '' search_message = ''
if 'query' in context and settings.DATABASES['default']['ENGINE'].endswith('sqlite'): if query_params['search_string'] and settings.DATABASES['default']['ENGINE'].endswith('sqlite'):
search_message = _( search_message = _(
'<p><strong>Note:</strong> Your keyword search is case sensitive ' '<p><strong>Note:</strong> Your keyword search is case sensitive '
'because of your database. This means the search will <strong>not</strong> ' 'because of your database. This means the search will <strong>not</strong> '
@ -985,31 +920,17 @@ 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>.')
import json
from helpdesk.lib import query_to_base64
urlsafe_query = query_to_base64(query_params)
user_saved_queries = SavedSearch.objects.filter(Q(user=request.user) | Q(shared__exact=True))
# Serverside processing on datatables is optional. Set
# HELPDESK_USE_SERVERSIDE_PROCESSING to False in settings.py to disable
if helpdesk_settings.HELPDESK_USE_SERVERSIDE_PROCESSING:
cache.set('ticket_qs', ticket_qs)
context['server_side'] = True
else:
context['server_side'] = False
return render(request, 'helpdesk/ticket_list.html', dict( return render(request, 'helpdesk/ticket_list.html', dict(
context, context,
tickets=ticket_qs,
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=user_queues, queue_choices=huser.get_queues(),
status_choices=Ticket.STATUS_CHOICES, status_choices=Ticket.STATUS_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,
from_saved_query=from_saved_query, from_saved_query=saved_query is not None,
saved_query=saved_query, saved_query=saved_query,
search_message=search_message, search_message=search_message,
)) ))
@ -1018,36 +939,57 @@ def ticket_list(request):
ticket_list = staff_member_required(ticket_list) ticket_list = staff_member_required(ticket_list)
class QueryLoadError(Exception):
pass
def load_saved_query(request, query_params=None):
saved_query = None
if request.GET.get('saved_query', None):
try:
saved_query = SavedSearch.objects.get(pk=request.GET.get('saved_query'))
except SavedSearch.DoesNotExist:
raise QueryLoadError()
if not (saved_query.shared or saved_query.user == request.user):
raise QueryLoadError()
try:
# we get a string like: b'stuff'
# so leave of the first two chars (b') and last (')
if saved_query.query.startswith('b\''):
b64query = saved_query.query[2:-1]
else:
b64query = saved_query.query
query_params = query_from_base64(b64query)
except json.JSONDecodeError:
raise QueryLoadError()
return (saved_query, query_params)
@helpdesk_staff_member_required @helpdesk_staff_member_required
@api_view(['GET', 'POST']) @api_view(['GET'])
def datatables_ticket_list(request): def datatables_ticket_list(request, query):
""" """
Datatable on ticket_list.html uses this view from to get objects to display Datatable on ticket_list.html uses this view from to get objects to display
on the table. query_tickets_by_args is at lib.py, TicketSerializer is in on the table. query_tickets_by_args is at lib.py, DatatablesTicketSerializer is in
serializers.py. The serializers and this view use django-rest_framework methods serializers.py. The serializers and this view use django-rest_framework methods
""" """
try: objects = get_query(query, HelpdeskUser(request.user))
objects = cache.get('ticket_qs') model_object = query_tickets_by_args(objects, '-date_created', **request.query_params)
model_object = query_tickets_by_args(objects, '-date_created', **request.query_params) serializer = DatatablesTicketSerializer(model_object['items'], many=True)
serializer = TicketSerializer(model_object['items'], many=True) result = dict()
result = dict() result['data'] = serializer.data
result['data'] = serializer.data result['draw'] = model_object['draw']
result['draw'] = model_object['draw'] result['recordsTotal'] = model_object['total']
result['recordsTotal'] = model_object['total'] result['recordsFiltered'] = model_object['count']
result['recordsFiltered'] = model_object['count'] return (JsonResponse(result, status=status.HTTP_200_OK))
return (Response(result, status=status.HTTP_200_OK, template_name=None, content_type=None))
except TypeError as e:
return (Response(e, status=status.HTTP_404_NOT_FOUND, template_name=None, content_type=None))
@helpdesk_staff_member_required @helpdesk_staff_member_required
def edit_ticket(request, ticket_id): def edit_ticket(request, ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if not _has_access_to_queue(request.user, ticket.queue): ticket_perm_check(request, ticket)
raise PermissionDenied()
if not _is_my_ticket(request.user, ticket):
raise PermissionDenied()
if request.method == 'POST': if request.method == 'POST':
form = EditTicketForm(request.POST, instance=ticket) form = EditTicketForm(request.POST, instance=ticket)
@ -1078,7 +1020,7 @@ class CreateTicketView(MustBeStaffMixin, FormView):
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
queues = _get_user_queues(self.request.user) queues = HelpdeskUser(self.request.user).get_queues()
kwargs["queue_choices"] = _get_queue_choices(queues) kwargs["queue_choices"] = _get_queue_choices(queues)
return kwargs return kwargs
@ -1088,7 +1030,7 @@ class CreateTicketView(MustBeStaffMixin, FormView):
def get_success_url(self): def get_success_url(self):
request = self.request request = self.request
if _has_access_to_queue(request.user, self.ticket.queue): if HelpdeskUser(request.user).can_access_queue(self.ticket.queue):
return self.ticket.get_absolute_url() return self.ticket.get_absolute_url()
else: else:
return reverse('helpdesk:dashboard') return reverse('helpdesk:dashboard')
@ -1119,10 +1061,7 @@ raw_details = staff_member_required(raw_details)
@helpdesk_staff_member_required @helpdesk_staff_member_required
def hold_ticket(request, ticket_id, unhold=False): def hold_ticket(request, ticket_id, unhold=False):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if not _has_access_to_queue(request.user, ticket.queue): ticket_perm_check(request, ticket)
raise PermissionDenied()
if not _is_my_ticket(request.user, ticket):
raise PermissionDenied()
if unhold: if unhold:
ticket.on_hold = False ticket.on_hold = False
@ -1169,7 +1108,7 @@ def report_index(request):
number_tickets = Ticket.objects.all().count() number_tickets = Ticket.objects.all().count()
saved_query = request.GET.get('saved_query', None) saved_query = request.GET.get('saved_query', None)
user_queues = _get_user_queues(request.user) user_queues = HelpdeskUser(request.user).get_queues()
Tickets = Ticket.objects.filter(queue__in=user_queues) Tickets = Ticket.objects.filter(queue__in=user_queues)
basic_ticket_stats = calc_basic_ticket_stats(Tickets) basic_ticket_stats = calc_basic_ticket_stats(Tickets)
@ -1212,31 +1151,15 @@ def run_report(request, report):
return HttpResponseRedirect(reverse("helpdesk:report_index")) return HttpResponseRedirect(reverse("helpdesk:report_index"))
report_queryset = Ticket.objects.all().select_related().filter( report_queryset = Ticket.objects.all().select_related().filter(
queue__in=_get_user_queues(request.user) queue__in=HelpdeskUser(request.user).get_queues()
) )
from_saved_query = False try:
saved_query = None saved_query, query_params = load_saved_query(request)
except QueryLoadError:
return HttpResponseRedirect(reverse('helpdesk:report_index'))
if request.GET.get('saved_query', None): if request.GET.get('saved_query', None):
from_saved_query = True
try:
saved_query = SavedSearch.objects.get(pk=request.GET.get('saved_query'))
except SavedSearch.DoesNotExist:
return HttpResponseRedirect(reverse('helpdesk:report_index'))
if not (saved_query.shared or saved_query.user == request.user):
return HttpResponseRedirect(reverse('helpdesk:report_index'))
import json
from helpdesk.lib import query_from_base64
try:
# we get a string like: b'stuff'
# so leave of the first two chars (b') and last (')
b64query = saved_query.query[2:-1]
query_params = query_from_base64(b64query)
except json.JSONDecodeError:
return HttpResponseRedirect(reverse('helpdesk:report_index'))
report_queryset = apply_query(report_queryset, query_params) report_queryset = apply_query(report_queryset, query_params)
from collections import defaultdict from collections import defaultdict
@ -1278,7 +1201,7 @@ def run_report(request, report):
elif report == 'userqueue': elif report == 'userqueue':
title = _('User by Queue') title = _('User by Queue')
col1heading = _('User') col1heading = _('User')
queue_options = _get_user_queues(request.user) queue_options = HelpdeskUser(request.user).get_queues()
possible_options = [q.title for q in queue_options] possible_options = [q.title for q in queue_options]
charttype = 'bar' charttype = 'bar'
@ -1399,7 +1322,7 @@ def run_report(request, report):
'headings': column_headings, 'headings': column_headings,
'series_names': series_names, 'series_names': series_names,
'morrisjs_data': morrisjs_data, 'morrisjs_data': morrisjs_data,
'from_saved_query': from_saved_query, 'from_saved_query': saved_query is not None,
'saved_query': saved_query, 'saved_query': saved_query,
}) })
@ -1493,10 +1416,7 @@ email_ignore_del = superuser_required(email_ignore_del)
@helpdesk_staff_member_required @helpdesk_staff_member_required
def ticket_cc(request, ticket_id): def ticket_cc(request, ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if not _has_access_to_queue(request.user, ticket.queue): ticket_perm_check(request, ticket)
raise PermissionDenied()
if not _is_my_ticket(request.user, ticket):
raise PermissionDenied()
copies_to = ticket.ticketcc_set.all() copies_to = ticket.ticketcc_set.all()
return render(request, 'helpdesk/ticket_cc_list.html', { return render(request, 'helpdesk/ticket_cc_list.html', {
@ -1511,10 +1431,7 @@ ticket_cc = staff_member_required(ticket_cc)
@helpdesk_staff_member_required @helpdesk_staff_member_required
def ticket_cc_add(request, ticket_id): def ticket_cc_add(request, ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if not _has_access_to_queue(request.user, ticket.queue): ticket_perm_check(request, ticket)
raise PermissionDenied()
if not _is_my_ticket(request.user, ticket):
raise PermissionDenied()
if request.method == 'POST': if request.method == 'POST':
form = TicketCCForm(request.POST) form = TicketCCForm(request.POST)
@ -1554,10 +1471,7 @@ ticket_cc_del = staff_member_required(ticket_cc_del)
@helpdesk_staff_member_required @helpdesk_staff_member_required
def ticket_dependency_add(request, ticket_id): def ticket_dependency_add(request, ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if not _has_access_to_queue(request.user, ticket.queue): ticket_perm_check(request, ticket)
raise PermissionDenied()
if not _is_my_ticket(request.user, ticket):
raise PermissionDenied()
if request.method == 'POST': if request.method == 'POST':
form = TicketDependencyForm(request.POST) form = TicketDependencyForm(request.POST)
if form.is_valid(): if form.is_valid():
@ -1592,10 +1506,7 @@ ticket_dependency_del = staff_member_required(ticket_dependency_del)
@helpdesk_staff_member_required @helpdesk_staff_member_required
def attachment_del(request, ticket_id, attachment_id): def attachment_del(request, ticket_id, attachment_id):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if not _has_access_to_queue(request.user, ticket.queue): ticket_perm_check(request, ticket)
raise PermissionDenied()
if not _is_my_ticket(request.user, ticket):
raise PermissionDenied()
attachment = get_object_or_404(FollowUpAttachment, id=attachment_id) attachment = get_object_or_404(FollowUpAttachment, id=attachment_id)
if request.method == 'POST': if request.method == 'POST':