Merge remote-tracking branch 'origin/main' into fix_unassigned_kbitems_not_visible_when_teams_not_active

This commit is contained in:
Christopher Broderick 2023-11-15 01:06:07 +00:00
commit 6cba903827
18 changed files with 465 additions and 437 deletions

View File

@ -28,21 +28,10 @@ jobs:
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements-testing.txt -c constraints-Django${{ matrix.django-version }}.txt pip install -r requirements.txt -r requirements-testing.txt -c constraints-Django${{ matrix.django-version }}.txt
- name: Format style check with 'autopep8' - name: Lint with ruff
run: | run: |
pip install autopep8 pip install ruff
autopep8 --exit-code --global-config .flake8 helpdesk ruff helpdesk
- name: Lint with 'flake8'
run: |
pip install flake8
# stop the build if there are Python syntax errors or undefined names
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 helpdesk --count --show-source --statistics --exit-zero --max-complexity=20
- name: Sort style check with 'isort'
run: |
isort --line-length=120 --src helpdesk . --check
- name: Test with pytest - name: Test with pytest
run: | run: |

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models, migrations from django.db import migrations
from helpdesk.settings import DEFAULT_USER_SETTINGS from helpdesk.settings import DEFAULT_USER_SETTINGS

View File

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
from sys import path
from django.db import models, migrations from django.db import migrations
from django.core import serializers from django.core import serializers
fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures')) fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures'))

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.db import models, migrations from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from helpdesk.models import Queue, Ticket, UserSettings from helpdesk.models import Queue, Ticket
import sys import sys

View File

@ -2,7 +2,7 @@
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from helpdesk.models import KBCategory, KBItem, Queue, Ticket from helpdesk.models import KBCategory, KBItem, Queue, Ticket
from helpdesk.tests.helpers import create_ticket, get_staff_user, print_response, reload_urlconf, User from helpdesk.tests.helpers import get_staff_user
class KBTests(TestCase): class KBTests(TestCase):

View File

@ -6,7 +6,7 @@ from django.test.utils import override_settings
from django.urls import reverse from django.urls import reverse
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.models import Queue from helpdesk.models import Queue
from helpdesk.tests.helpers import create_ticket, get_staff_user, print_response, reload_urlconf, User from helpdesk.tests.helpers import create_ticket, get_staff_user, reload_urlconf, User
from importlib import reload from importlib import reload
import sys import sys
@ -109,7 +109,6 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
"""When HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False, """When HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False,
staff users should be able to access the dashboard. staff users should be able to access the dashboard.
""" """
from helpdesk.decorators import is_helpdesk_staff
user = get_staff_user() user = get_staff_user()
self.client.login(username=user.username, password='password') self.client.login(username=user.username, password='password')
@ -120,8 +119,6 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
"""When HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False, """When HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False,
non-staff users should not be able to access the dashboard. non-staff users should not be able to access the dashboard.
""" """
from helpdesk.decorators import is_helpdesk_staff
user = self.non_staff_user user = self.non_staff_user
self.client.login(username=user.username, self.client.login(username=user.username,
password=self.non_staff_user_password) password=self.non_staff_user_password)

View File

@ -3,7 +3,7 @@ from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from helpdesk.models import KBCategory, KBItem, Queue, Ticket from helpdesk.models import KBCategory, KBItem, Queue, Ticket
from helpdesk.query import query_to_base64 from helpdesk.query import query_to_base64
from helpdesk.tests.helpers import create_ticket, get_staff_user, print_response, reload_urlconf, User from helpdesk.tests.helpers import get_staff_user
class QueryTests(TestCase): class QueryTests(TestCase):

View File

@ -1,25 +1,12 @@
import datetime import datetime
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core import mail
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.urls import reverse
from helpdesk import settings as helpdesk_settings
from helpdesk.models import FollowUp, Queue, Ticket from helpdesk.models import FollowUp, Queue, Ticket
from helpdesk.templatetags.ticket_to_link import num_to_link
import uuid import uuid
try: # python 3
from urllib.parse import urlparse
except ImportError: # python 2
from urlparse import urlparse
class TimeSpentTestCase(TestCase): class TimeSpentTestCase(TestCase):
def setUp(self): def setUp(self):

View File

@ -1,15 +1,6 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core import mail
from django.test import TestCase from django.test import TestCase
from django.test.client import Client
from django.urls import reverse from django.urls import reverse
from helpdesk.models import CustomField, Queue, Ticket
try: # python 3
from urllib.parse import urlparse
except ImportError: # python 2
from urlparse import urlparse
class TicketActionsTestCase(TestCase): class TicketActionsTestCase(TestCase):

389
helpdesk/update_ticket.py Normal file
View File

@ -0,0 +1,389 @@
import typing
from django.core.exceptions import ValidationError
from django.contrib.auth import get_user_model
from django.utils import timezone
from django.utils.translation import gettext as _
from helpdesk.lib import safe_template_context
from helpdesk import settings as helpdesk_settings
from helpdesk.lib import process_attachments
from helpdesk.decorators import (
is_helpdesk_staff,
)
from helpdesk.models import (
FollowUp,
Ticket,
TicketCC,
TicketChange,
)
User = get_user_model()
def add_staff_subscription(
user: User,
ticket: Ticket
) -> None:
"""Auto subscribe the staff member if that's what the settigs say and the
user is authenticated and a staff member"""
if helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE \
and user.is_authenticated \
and return_ticketccstring_and_show_subscribe(user, ticket)[1]:
subscribe_to_ticket_updates(ticket, user)
def return_ticketccstring_and_show_subscribe(user, ticket):
"""used in view_ticket() and followup_edit()"""
# create the ticketcc_string and check whether current user is already
# subscribed
username = user.get_username().upper()
try:
useremail = user.email.upper()
except AttributeError:
useremail = ""
strings_to_check = list()
strings_to_check.append(username)
strings_to_check.append(useremail)
ticketcc_string = ''
all_ticketcc = ticket.ticketcc_set.all()
counter_all_ticketcc = len(all_ticketcc) - 1
show_subscribe = True
for i, ticketcc in enumerate(all_ticketcc):
ticketcc_this_entry = str(ticketcc.display)
ticketcc_string += ticketcc_this_entry
if i < counter_all_ticketcc:
ticketcc_string += ', '
if strings_to_check.__contains__(ticketcc_this_entry.upper()):
show_subscribe = False
# check whether current user is a submitter or assigned to ticket
assignedto_username = str(ticket.assigned_to).upper()
strings_to_check = list()
if ticket.submitter_email is not None:
submitter_email = ticket.submitter_email.upper()
strings_to_check.append(submitter_email)
strings_to_check.append(assignedto_username)
if strings_to_check.__contains__(username) or strings_to_check.__contains__(useremail):
show_subscribe = False
return ticketcc_string, show_subscribe
def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, can_update=False):
if ticket is not None:
queryset = TicketCC.objects.filter(
ticket=ticket, user=user, email=email)
# Don't create duplicate entries for subscribers
if queryset.count() > 0:
return queryset.first()
if user is None and len(email) < 5:
raise ValidationError(
_('When you add somebody on Cc, you must provide either a User or a valid email. Email: %s' % email)
)
return ticket.ticketcc_set.create(
user=user,
email=email,
can_view=can_view,
can_update=can_update
)
def get_and_set_ticket_status(
new_status: int,
ticket: Ticket,
follow_up: FollowUp
) -> typing.Tuple[str, int]:
"""Performs comparision on previous status to new status,
updating the title as required.
Returns:
The old status as a display string, old status code string
"""
old_status_str = ticket.get_status_display()
old_status = ticket.status
if new_status != ticket.status:
ticket.status = new_status
ticket.save()
follow_up.new_status = new_status
if follow_up.title:
follow_up.title += ' and %s' % ticket.get_status_display()
else:
follow_up.title = '%s' % ticket.get_status_display()
if not follow_up.title:
if follow_up.comment:
follow_up.title = _('Comment')
else:
follow_up.title = _('Updated')
follow_up.save()
return old_status_str, old_status
def update_messages_sent_to_by_public_and_status(
public: bool,
ticket: Ticket,
follow_up: FollowUp,
context: str,
messages_sent_to: typing.Set[str],
files: typing.List[typing.Tuple[str, str]]
) -> Ticket:
"""Sets the status of the ticket"""
if public and (
follow_up.comment or (
follow_up.new_status in (
Ticket.RESOLVED_STATUS,
Ticket.CLOSED_STATUS
)
)
):
if follow_up.new_status == Ticket.RESOLVED_STATUS:
template = 'resolved_'
elif follow_up.new_status == Ticket.CLOSED_STATUS:
template = 'closed_'
else:
template = 'updated_'
roles = {
'submitter': (template + 'submitter', context),
'ticket_cc': (template + 'cc', context),
}
if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change:
roles['assigned_to'] = (template + 'cc', context)
messages_sent_to.update(
ticket.send(
roles,
dont_send_to=messages_sent_to,
fail_silently=True,
files=files
)
)
return ticket
def get_template_staff_and_template_cc(
reassigned, follow_up: FollowUp
) -> typing.Tuple[str, str]:
if reassigned:
template_staff = 'assigned_owner'
elif follow_up.new_status == Ticket.RESOLVED_STATUS:
template_staff = 'resolved_owner'
elif follow_up.new_status == Ticket.CLOSED_STATUS:
template_staff = 'closed_owner'
else:
template_staff = 'updated_owner'
if reassigned:
template_cc = 'assigned_cc'
elif follow_up.new_status == Ticket.RESOLVED_STATUS:
template_cc = 'resolved_cc'
elif follow_up.new_status == Ticket.CLOSED_STATUS:
template_cc = 'closed_cc'
else:
template_cc = 'updated_cc'
return template_staff, template_cc
def update_ticket(
user,
ticket,
title=None,
comment="",
files=None,
public=False,
owner=-1,
priority=-1,
new_status=None,
time_spent=None,
due_date=None,
new_checklists=None,
):
# We need to allow the 'ticket' and 'queue' contexts to be applied to the
# comment.
context = safe_template_context(ticket)
if title is None:
title = ticket.title
if priority == -1:
priority = ticket.priority
if new_status is None:
new_status = ticket.status
if new_checklists is None:
new_checklists = {}
from django.template import engines
template_func = engines['django'].from_string
# this prevents system from trying to render any template tags
# broken into two stages to prevent changes from first replace being themselves
# changed by the second replace due to conflicting syntax
comment = comment.replace(
'{%', 'X-HELPDESK-COMMENT-VERBATIM').replace('%}', 'X-HELPDESK-COMMENT-ENDVERBATIM')
comment = comment.replace(
'X-HELPDESK-COMMENT-VERBATIM', '{% verbatim %}{%'
).replace(
'X-HELPDESK-COMMENT-ENDVERBATIM', '%}{% endverbatim %}'
)
# render the neutralized template
comment = template_func(comment).render(context)
if owner == -1 and ticket.assigned_to:
owner = ticket.assigned_to.id
f = FollowUp(ticket=ticket, date=timezone.now(), comment=comment,
time_spent=time_spent)
if is_helpdesk_staff(user):
f.user = user
f.public = public
reassigned = False
old_owner = ticket.assigned_to
if owner != -1:
if owner != 0 and ((ticket.assigned_to and owner != ticket.assigned_to.id) or not ticket.assigned_to):
new_user = User.objects.get(id=owner)
f.title = _('Assigned to %(username)s') % {
'username': new_user.get_username(),
}
ticket.assigned_to = new_user
reassigned = True
# user changed owner to 'unassign'
elif owner == 0 and ticket.assigned_to is not None:
f.title = _('Unassigned')
ticket.assigned_to = None
old_status_str, old_status = get_and_set_ticket_status(new_status, ticket, f)
files = process_attachments(f, files) if files else []
if title and title != ticket.title:
c = TicketChange(
followup=f,
field=_('Title'),
old_value=ticket.title,
new_value=title,
)
c.save()
ticket.title = title
if new_status != old_status:
c = TicketChange(
followup=f,
field=_('Status'),
old_value=old_status_str,
new_value=ticket.get_status_display(),
)
c.save()
if ticket.assigned_to != old_owner:
c = TicketChange(
followup=f,
field=_('Owner'),
old_value=old_owner,
new_value=ticket.assigned_to,
)
c.save()
if priority != ticket.priority:
c = TicketChange(
followup=f,
field=_('Priority'),
old_value=ticket.priority,
new_value=priority,
)
c.save()
ticket.priority = priority
if due_date != ticket.due_date:
c = TicketChange(
followup=f,
field=_('Due on'),
old_value=ticket.due_date,
new_value=due_date,
)
c.save()
ticket.due_date = due_date
for checklist in ticket.checklists.all():
if checklist.id not in new_checklists:
continue
new_completed_tasks = new_checklists[checklist.id]
for task in checklist.tasks.all():
changed = None
# Add completion if it was not done yet
if not task.completion_date and task.id in new_completed_tasks:
task.completion_date = timezone.now()
changed = 'completed'
# Remove it if it was done before
elif task.completion_date and task.id not in new_completed_tasks:
task.completion_date = None
changed = 'uncompleted'
# Save and add ticket change if task state has changed
if changed:
task.save(update_fields=['completion_date'])
f.ticketchange_set.create(
field=f'[{checklist.name}] {task.description}',
old_value=_('To do') if changed == 'completed' else _('Completed'),
new_value=_('Completed') if changed == 'completed' else _('To do'),
)
if new_status in (
Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS
) and (
new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None
):
ticket.resolution = comment
# ticket might have changed above, so we re-instantiate context with the
# (possibly) updated ticket.
context = safe_template_context(ticket)
context.update(
resolution=ticket.resolution,
comment=f.comment,
)
messages_sent_to = set()
try:
messages_sent_to.add(user.email)
except AttributeError:
pass
ticket = update_messages_sent_to_by_public_and_status(
public,
ticket,
f,
context,
messages_sent_to,
files
)
template_staff, template_cc = get_template_staff_and_template_cc(reassigned, f)
if ticket.assigned_to and (
ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change
or (reassigned and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign)
):
messages_sent_to.update(ticket.send(
{'assigned_to': (template_staff, context)},
dont_send_to=messages_sent_to,
fail_silently=True,
files=files,
))
messages_sent_to.update(ticket.send(
{'ticket_cc': (template_cc, context)},
dont_send_to=messages_sent_to,
fail_silently=True,
files=files,
))
ticket.save()
# auto subscribe user if enabled
add_staff_subscription(user, ticket)

View File

@ -64,7 +64,7 @@ urlpatterns = [
), ),
path("tickets/<int:ticket_id>/edit/", staff.edit_ticket, name="edit"), path("tickets/<int:ticket_id>/edit/", staff.edit_ticket, name="edit"),
path("tickets/<int:ticket_id>/update/", path("tickets/<int:ticket_id>/update/",
staff.update_ticket, name="update"), staff.update_ticket_view, name="update"),
path("tickets/<int:ticket_id>/delete/", path("tickets/<int:ticket_id>/delete/",
staff.delete_ticket, name="delete"), staff.delete_ticket, name="delete"),
path("tickets/<int:ticket_id>/hold/", staff.hold_ticket, name="hold"), path("tickets/<int:ticket_id>/hold/", staff.hold_ticket, name="hold"),

View File

@ -28,7 +28,7 @@ def validate_file_extension(value):
valid_extensions = ['.txt', '.asc', '.htm', '.html', valid_extensions = ['.txt', '.asc', '.htm', '.html',
'.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml'] '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml']
if not ext.lower() in valid_extensions: if ext.lower() not in valid_extensions:
# TODO: one more check in case it is a file with no extension; we # TODO: one more check in case it is a file with no extension; we
# should always allow that? # should always allow that?
if not (ext.lower() == '' or ext.lower() == '.'): if not (ext.lower() == '' or ext.lower() == '.'):

View File

@ -210,21 +210,16 @@ def view_ticket(request):
return HttpResponseRedirect(redirect_url) return HttpResponseRedirect(redirect_url)
if 'close' in request.GET and ticket.status == Ticket.RESOLVED_STATUS: if 'close' in request.GET and ticket.status == Ticket.RESOLVED_STATUS:
from helpdesk.views.staff import update_ticket from helpdesk.update_ticket import update_ticket
update_ticket(
request.user,
ticket,
public = True,
comment = _('Submitter accepted resolution and closed ticket'),
new_status = Ticket.CLOSED_STATUS,
)
return HttpResponseRedirect(ticket.ticket_url)
# Trick the update_ticket() view into thinking it's being called with
# a valid POST.
request.POST = {
'new_status': Ticket.CLOSED_STATUS,
'public': 1,
'title': ticket.title,
'comment': _('Submitter accepted resolution and closed ticket'),
}
if ticket.assigned_to:
request.POST['owner'] = ticket.assigned_to.id
request.GET = {}
return update_ticket(request, ticket_id, public=True)
# redirect user back to this ticket if possible. # redirect user back to this ticket if possible.
redirect_url = '' redirect_url = ''

View File

@ -17,7 +17,7 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import user_passes_test from django.contrib.auth.decorators import user_passes_test
from django.contrib.auth.views import redirect_to_login from django.contrib.auth.views import redirect_to_login
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied
from django.core.handlers.wsgi import WSGIRequest from django.core.handlers.wsgi import WSGIRequest
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db.models import Q from django.db.models import Q
@ -56,7 +56,7 @@ from helpdesk.forms import (
TicketForm, TicketForm,
UserSettingsForm UserSettingsForm
) )
from helpdesk.lib import process_attachments, queue_template_context, safe_template_context from helpdesk.lib import queue_template_context, safe_template_context
from helpdesk.models import ( from helpdesk.models import (
Checklist, Checklist,
ChecklistTask, ChecklistTask,
@ -70,13 +70,13 @@ from helpdesk.models import (
SavedSearch, SavedSearch,
Ticket, Ticket,
TicketCC, TicketCC,
TicketChange,
TicketCustomFieldValue, TicketCustomFieldValue,
TicketDependency, TicketDependency,
UserSettings UserSettings
) )
from helpdesk.query import get_query_class, query_from_base64, query_to_base64 from helpdesk.query import get_query_class, query_from_base64, query_to_base64
from helpdesk.user import HelpdeskUser from helpdesk.user import HelpdeskUser
from helpdesk.update_ticket import update_ticket, subscribe_to_ticket_updates, return_ticketccstring_and_show_subscribe
import helpdesk.views.abstract_views as abstract_views import helpdesk.views.abstract_views as abstract_views
from helpdesk.views.permissions import MustBeStaffMixin from helpdesk.views.permissions import MustBeStaffMixin
import json import json
@ -350,17 +350,12 @@ def view_ticket(request, ticket_id):
ticket_perm_check(request, ticket) ticket_perm_check(request, ticket)
if 'take' in request.GET: if 'take' in request.GET:
# Allow the user to assign the ticket to themselves whilst viewing it. update_ticket(
request.user,
# Trick the update_ticket() view into thinking it's being called with ticket,
# a valid POST. owner=request.user.id
request.POST = { )
'owner': request.user.id, return return_to_ticket(request.user, helpdesk_settings, ticket)
'public': 1,
'title': ticket.title,
'comment': ''
}
return update_ticket(request, ticket_id)
if 'subscribe' in request.GET: if 'subscribe' in request.GET:
# Allow the user to subscribe him/herself to the ticket whilst viewing # Allow the user to subscribe him/herself to the ticket whilst viewing
@ -379,17 +374,13 @@ def view_ticket(request, ticket_id):
else: else:
owner = ticket.assigned_to.id owner = ticket.assigned_to.id
# Trick the update_ticket() view into thinking it's being called with update_ticket(
# a valid POST. request.user,
request.POST = { ticket,
'new_status': Ticket.CLOSED_STATUS, owner=owner,
'public': 1, comment= _('Accepted resolution and closed ticket'),
'owner': owner, )
'title': ticket.title, return return_to_ticket(request.user, helpdesk_settings, ticket)
'comment': _('Accepted resolution and closed ticket'),
}
return update_ticket(request, ticket_id)
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS: if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS:
users = User.objects.filter( users = User.objects.filter(
@ -494,68 +485,6 @@ def delete_ticket_checklist(request, ticket_id, checklist_id):
}) })
def return_ticketccstring_and_show_subscribe(user, ticket):
"""used in view_ticket() and followup_edit()"""
# create the ticketcc_string and check whether current user is already
# subscribed
username = user.get_username().upper()
try:
useremail = user.email.upper()
except AttributeError:
useremail = ""
strings_to_check = list()
strings_to_check.append(username)
strings_to_check.append(useremail)
ticketcc_string = ''
all_ticketcc = ticket.ticketcc_set.all()
counter_all_ticketcc = len(all_ticketcc) - 1
show_subscribe = True
for i, ticketcc in enumerate(all_ticketcc):
ticketcc_this_entry = str(ticketcc.display)
ticketcc_string += ticketcc_this_entry
if i < counter_all_ticketcc:
ticketcc_string += ', '
if strings_to_check.__contains__(ticketcc_this_entry.upper()):
show_subscribe = False
# check whether current user is a submitter or assigned to ticket
assignedto_username = str(ticket.assigned_to).upper()
strings_to_check = list()
if ticket.submitter_email is not None:
submitter_email = ticket.submitter_email.upper()
strings_to_check.append(submitter_email)
strings_to_check.append(assignedto_username)
if strings_to_check.__contains__(username) or strings_to_check.__contains__(useremail):
show_subscribe = False
return ticketcc_string, show_subscribe
def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, can_update=False):
if ticket is not None:
queryset = TicketCC.objects.filter(
ticket=ticket, user=user, email=email)
# Don't create duplicate entries for subscribers
if queryset.count() > 0:
return queryset.first()
if user is None and len(email) < 5:
raise ValidationError(
_('When you add somebody on Cc, you must provide either a User or a valid email. Email: %s' % email)
)
return ticket.ticketcc_set.create(
user=user,
email=email,
can_view=can_view,
can_update=can_update
)
def get_ticket_from_request_with_authorisation( def get_ticket_from_request_with_authorisation(
request: WSGIRequest, request: WSGIRequest,
ticket_id: str, ticket_id: str,
@ -617,38 +546,6 @@ def get_due_date_from_request_or_ticket(
return due_date return due_date
def get_and_set_ticket_status(
new_status: int,
ticket: Ticket,
follow_up: FollowUp
) -> typing.Tuple[str, int]:
"""Performs comparision on previous status to new status,
updating the title as required.
Returns:
The old status as a display string, old status code string
"""
old_status_str = ticket.get_status_display()
old_status = ticket.status
if new_status != ticket.status:
ticket.status = new_status
ticket.save()
follow_up.new_status = new_status
if follow_up.title:
follow_up.title += ' and %s' % ticket.get_status_display()
else:
follow_up.title = '%s' % ticket.get_status_display()
if not follow_up.title:
if follow_up.comment:
follow_up.title = _('Comment')
else:
follow_up.title = _('Updated')
follow_up.save()
return old_status_str, old_status
def get_time_spent_from_request(request: WSGIRequest) -> typing.Optional[timedelta]: def get_time_spent_from_request(request: WSGIRequest) -> typing.Optional[timedelta]:
if request.POST.get("time_spent"): if request.POST.get("time_spent"):
(hours, minutes) = [int(f) (hours, minutes) = [int(f)
@ -657,83 +554,7 @@ def get_time_spent_from_request(request: WSGIRequest) -> typing.Optional[timedel
return None return None
def update_messages_sent_to_by_public_and_status( def update_ticket_view(request, ticket_id, public=False):
public: bool,
ticket: Ticket,
follow_up: FollowUp,
context: str,
messages_sent_to: typing.Set[str],
files: typing.List[typing.Tuple[str, str]]
) -> Ticket:
"""Sets the status of the ticket"""
if public and (
follow_up.comment or (
follow_up.new_status in (
Ticket.RESOLVED_STATUS,
Ticket.CLOSED_STATUS
)
)
):
if follow_up.new_status == Ticket.RESOLVED_STATUS:
template = 'resolved_'
elif follow_up.new_status == Ticket.CLOSED_STATUS:
template = 'closed_'
else:
template = 'updated_'
roles = {
'submitter': (template + 'submitter', context),
'ticket_cc': (template + 'cc', context),
}
if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change:
roles['assigned_to'] = (template + 'cc', context)
messages_sent_to.update(
ticket.send(
roles,
dont_send_to=messages_sent_to,
fail_silently=True,
files=files
)
)
return ticket
def add_staff_subscription(
request: WSGIRequest,
ticket: Ticket
) -> None:
"""Auto subscribe the staff member if that's what the settigs say and the
user is authenticated and a staff member"""
if helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE \
and request.user.is_authenticated \
and return_ticketccstring_and_show_subscribe(request.user, ticket)[1]:
subscribe_to_ticket_updates(ticket, request.user)
def get_template_staff_and_template_cc(
reassigned, follow_up: FollowUp
) -> typing.Tuple[str, str]:
if reassigned:
template_staff = 'assigned_owner'
elif follow_up.new_status == Ticket.RESOLVED_STATUS:
template_staff = 'resolved_owner'
elif follow_up.new_status == Ticket.CLOSED_STATUS:
template_staff = 'closed_owner'
else:
template_staff = 'updated_owner'
if reassigned:
template_cc = 'assigned_cc'
elif follow_up.new_status == Ticket.RESOLVED_STATUS:
template_cc = 'resolved_cc'
elif follow_up.new_status == Ticket.CLOSED_STATUS:
template_cc = 'closed_cc'
else:
template_cc = 'updated_cc'
return template_staff, template_cc
def update_ticket(request, ticket_id, public=False):
try: try:
ticket = get_ticket_from_request_with_authorisation(request, ticket_id, public) ticket = get_ticket_from_request_with_authorisation(request, ticket_id, public)
@ -743,16 +564,17 @@ def update_ticket(request, ticket_id, public=False):
comment = request.POST.get('comment', '') comment = request.POST.get('comment', '')
new_status = int(request.POST.get('new_status', ticket.status)) new_status = int(request.POST.get('new_status', ticket.status))
title = request.POST.get('title', '') title = request.POST.get('title', '')
public = request.POST.get('public', False)
owner = int(request.POST.get('owner', -1)) owner = int(request.POST.get('owner', -1))
priority = int(request.POST.get('priority', ticket.priority)) priority = int(request.POST.get('priority', ticket.priority))
# Check if a change happened on checklists # Check if a change happened on checklists
new_checklists = {}
changes_in_checklists = False changes_in_checklists = False
for checklist in ticket.checklists.all(): for checklist in ticket.checklists.all():
old_completed_id = sorted(list(checklist.tasks.completed().values_list('id', flat=True))) old_completed = set(checklist.tasks.completed().values_list('id', flat=True))
new_completed_id = sorted(list(map(int, request.POST.getlist(f'checklist-{checklist.id}', [])))) new_checklist = set(map(int, request.POST.getlist(f'checklist-{checklist.id}', [])))
if old_completed_id != new_completed_id: new_checklists[checklist.id] = new_checklist
if new_checklist != old_completed:
changes_in_checklists = True changes_in_checklists = True
time_spent = get_time_spent_from_request(request) time_spent = get_time_spent_from_request(request)
@ -774,180 +596,21 @@ def update_ticket(request, ticket_id, public=False):
if no_changes: if no_changes:
return return_to_ticket(request.user, helpdesk_settings, ticket) return return_to_ticket(request.user, helpdesk_settings, ticket)
# We need to allow the 'ticket' and 'queue' contexts to be applied to the update_ticket(
# comment. request.user,
context = safe_template_context(ticket)
from django.template import engines
template_func = engines['django'].from_string
# this prevents system from trying to render any template tags
# broken into two stages to prevent changes from first replace being themselves
# changed by the second replace due to conflicting syntax
comment = comment.replace(
'{%', 'X-HELPDESK-COMMENT-VERBATIM').replace('%}', 'X-HELPDESK-COMMENT-ENDVERBATIM')
comment = comment.replace(
'X-HELPDESK-COMMENT-VERBATIM', '{% verbatim %}{%'
).replace(
'X-HELPDESK-COMMENT-ENDVERBATIM', '%}{% endverbatim %}'
)
# render the neutralized template
comment = template_func(comment).render(context)
if owner == -1 and ticket.assigned_to:
owner = ticket.assigned_to.id
f = FollowUp(ticket=ticket, date=timezone.now(), comment=comment,
time_spent=time_spent)
if is_helpdesk_staff(request.user):
f.user = request.user
f.public = public
reassigned = False
old_owner = ticket.assigned_to
if owner != -1:
if owner != 0 and ((ticket.assigned_to and owner != ticket.assigned_to.id) or not ticket.assigned_to):
new_user = User.objects.get(id=owner)
f.title = _('Assigned to %(username)s') % {
'username': new_user.get_username(),
}
ticket.assigned_to = new_user
reassigned = True
# user changed owner to 'unassign'
elif owner == 0 and ticket.assigned_to is not None:
f.title = _('Unassigned')
ticket.assigned_to = None
old_status_str, old_status = get_and_set_ticket_status(new_status, ticket, f)
files = process_attachments(f, request.FILES.getlist('attachment')) if request.FILES else []
if title and title != ticket.title:
c = TicketChange(
followup=f,
field=_('Title'),
old_value=ticket.title,
new_value=title,
)
c.save()
ticket.title = title
if new_status != old_status:
c = TicketChange(
followup=f,
field=_('Status'),
old_value=old_status_str,
new_value=ticket.get_status_display(),
)
c.save()
if ticket.assigned_to != old_owner:
c = TicketChange(
followup=f,
field=_('Owner'),
old_value=old_owner,
new_value=ticket.assigned_to,
)
c.save()
if priority != ticket.priority:
c = TicketChange(
followup=f,
field=_('Priority'),
old_value=ticket.priority,
new_value=priority,
)
c.save()
ticket.priority = priority
if due_date != ticket.due_date:
c = TicketChange(
followup=f,
field=_('Due on'),
old_value=ticket.due_date,
new_value=due_date,
)
c.save()
ticket.due_date = due_date
if changes_in_checklists:
for checklist in ticket.checklists.all():
new_completed_tasks = list(map(int, request.POST.getlist(f'checklist-{checklist.id}', [])))
for task in checklist.tasks.all():
changed = None
# Add completion if it was not done yet
if not task.completion_date and task.id in new_completed_tasks:
task.completion_date = timezone.now()
changed = 'completed'
# Remove it if it was done before
elif task.completion_date and task.id not in new_completed_tasks:
task.completion_date = None
changed = 'uncompleted'
# Save and add ticket change if task state has changed
if changed:
task.save(update_fields=['completion_date'])
f.ticketchange_set.create(
field=f'[{checklist.name}] {task.description}',
old_value=_('To do') if changed == 'completed' else _('Completed'),
new_value=_('Completed') if changed == 'completed' else _('To do'),
)
if new_status in (
Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS
) and (
new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None
):
ticket.resolution = comment
# ticket might have changed above, so we re-instantiate context with the
# (possibly) updated ticket.
context = safe_template_context(ticket)
context.update(
resolution=ticket.resolution,
comment=f.comment,
)
messages_sent_to = set()
try:
messages_sent_to.add(request.user.email)
except AttributeError:
pass
ticket = update_messages_sent_to_by_public_and_status(
public,
ticket, ticket,
f, title = title,
context, comment = comment,
messages_sent_to, files = request.FILES.getlist('attachment'),
files public = request.POST.get('public', False),
owner = int(request.POST.get('owner', -1)),
priority = int(request.POST.get('priority', -1)),
new_status = new_status,
time_spent = get_time_spent_from_request(request),
due_date = get_due_date_from_request_or_ticket(request, ticket),
new_checklists = new_checklists,
) )
template_staff, template_cc = get_template_staff_and_template_cc(reassigned, f)
if ticket.assigned_to and (
ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change
or (reassigned and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign)
):
messages_sent_to.update(ticket.send(
{'assigned_to': (template_staff, context)},
dont_send_to=messages_sent_to,
fail_silently=True,
files=files,
))
messages_sent_to.update(ticket.send(
{'ticket_cc': (template_cc, context)},
dont_send_to=messages_sent_to,
fail_silently=True,
files=files,
))
ticket.save()
# auto subscribe user if enabled
add_staff_subscription(request, ticket)
return return_to_ticket(request.user, helpdesk_settings, ticket) return return_to_ticket(request.user, helpdesk_settings, ticket)
@ -1346,7 +1009,7 @@ def ticket_list(request):
('kbitem', 'kbitem__isnull'), ('kbitem', 'kbitem__isnull'),
]) ])
for param, filter_command in filter_in_params: for param, filter_command in filter_in_params:
if not request.GET.get(param) is None: if request.GET.get(param) is not None:
patterns = request.GET.getlist(param) patterns = request.GET.getlist(param)
try: try:
pattern_pks = [int(pattern) for pattern in patterns] pattern_pks = [int(pattern) for pattern in patterns]

2
pyproject.toml Normal file
View File

@ -0,0 +1,2 @@
[tool.ruff]
ignore = ["E501", "E731", "F841", "E721"]

View File

@ -1,6 +1,5 @@
tox tox
autopep8 ruff
flake8
pycodestyle pycodestyle
isort isort
freezegun freezegun

View File

@ -229,3 +229,20 @@ MEDIA_ROOT = '/data/media'
# for Django 3.2+, set default for autofields: # for Django 3.2+, set default for autofields:
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django': {
'handlers': ['console'],
'level': 'ERROR', # Change to 'DEBUG' if you want to print all debug messages as well
'propagate': True,
},
},
}