mirror of
https://gitea.mueller.network/extern/django-helpdesk.git
synced 2024-11-21 23:43:11 +01:00
Merge remote-tracking branch 'origin/main' into fix_unassigned_kbitems_not_visible_when_teams_not_active
This commit is contained in:
commit
6cba903827
17
.github/workflows/pythonpackage.yml
vendored
17
.github/workflows/pythonpackage.yml
vendored
@ -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: |
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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'))
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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
389
helpdesk/update_ticket.py
Normal 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)
|
||||||
|
|
@ -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"),
|
||||||
|
@ -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() == '.'):
|
||||||
|
@ -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 = ''
|
||||||
|
@ -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
2
pyproject.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[tool.ruff]
|
||||||
|
ignore = ["E501", "E731", "F841", "E721"]
|
@ -1,6 +1,5 @@
|
|||||||
tox
|
tox
|
||||||
autopep8
|
ruff
|
||||||
flake8
|
|
||||||
pycodestyle
|
pycodestyle
|
||||||
isort
|
isort
|
||||||
freezegun
|
freezegun
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user