mirror of
https://github.com/django-helpdesk/django-helpdesk.git
synced 2025-06-03 00:15:46 +02:00
Merge pull request #1249 from DavidVadnais/BUG-1187-Fix-make-check-format
Bug 1187 fix make check format
This commit is contained in:
commit
f3cfd0ec4e
7
.github/workflows/pythonpackage.yml
vendored
7
.github/workflows/pythonpackage.yml
vendored
@ -9,9 +9,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# Explicitly include Python 3.8 and 3.9 only with Django 4
|
||||
- python-version: "3.8"
|
||||
django-version: "4"
|
||||
# Explicitly include Python 3.9 only with Django 4
|
||||
- python-version: "3.9"
|
||||
django-version: "4"
|
||||
# Define the general matrix for Python with Django
|
||||
@ -38,7 +36,8 @@ jobs:
|
||||
- name: Lint with ruff
|
||||
run: |
|
||||
pip install ruff
|
||||
ruff check helpdesk
|
||||
make checkformat
|
||||
|
||||
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
|
2
LICENSE
2
LICENSE
@ -1,5 +1,5 @@
|
||||
Copyright (c) 2008 Ross Poulton (Trading as Jutda),
|
||||
Copyright (c) 2008-2021 django-helpdesk contributors.
|
||||
Copyright (c) 2008-2025 django-helpdesk contributors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
|
6
Makefile
6
Makefile
@ -63,15 +63,13 @@ test:
|
||||
#: format - Run the PEP8 formatter.
|
||||
.PHONY: format
|
||||
format:
|
||||
autopep8 --exit-code --global-config .flake8 helpdesk
|
||||
isort --line-length=120 --src helpdesk .
|
||||
ruff format helpdesk
|
||||
|
||||
|
||||
#: checkformat - checks formatting against configured format specifications for the project.
|
||||
.PHONY: checkformat
|
||||
checkformat:
|
||||
flake8 helpdesk --count --show-source --statistics --exit-zero --max-complexity=20
|
||||
isort --line-length=120 --src helpdesk . --check
|
||||
ruff check helpdesk
|
||||
|
||||
|
||||
#: documentation - Build documentation (Sphinx, README, ...).
|
||||
|
@ -8,7 +8,7 @@ django-helpdesk - A Django powered ticket tracker for small businesses.
|
||||
.. image:: https://codecov.io/gh/django-helpdesk/django-helpdesk/branch/develop/graph/badge.svg
|
||||
:target: https://codecov.io/gh/django-helpdesk/django-helpdesk
|
||||
|
||||
Copyright 2009-2023 Ross Poulton and django-helpdesk contributors. All Rights Reserved.
|
||||
Copyright 2009-2025 Ross Poulton and django-helpdesk contributors. All Rights Reserved.
|
||||
See LICENSE for details.
|
||||
|
||||
django-helpdesk was formerly known as Jutda Helpdesk, named after the
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
@ -16,7 +15,7 @@ from helpdesk.models import (
|
||||
PreSetReply,
|
||||
Queue,
|
||||
Ticket,
|
||||
TicketChange
|
||||
TicketChange,
|
||||
)
|
||||
|
||||
|
||||
@ -26,7 +25,7 @@ if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
|
||||
@admin.register(Queue)
|
||||
class QueueAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'slug', 'email_address', 'locale', 'time_spent')
|
||||
list_display = ("title", "slug", "email_address", "locale", "time_spent")
|
||||
prepopulated_fields = {"slug": ("title",)}
|
||||
|
||||
def time_spent(self, q):
|
||||
@ -44,11 +43,17 @@ class QueueAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Ticket)
|
||||
class TicketAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'status', 'assigned_to', 'queue',
|
||||
'hidden_submitter_email', 'time_spent')
|
||||
date_hierarchy = 'created'
|
||||
list_filter = ('queue', 'assigned_to', 'status')
|
||||
search_fields = ('id', 'title')
|
||||
list_display = (
|
||||
"title",
|
||||
"status",
|
||||
"assigned_to",
|
||||
"queue",
|
||||
"hidden_submitter_email",
|
||||
"time_spent",
|
||||
)
|
||||
date_hierarchy = "created"
|
||||
list_filter = ("queue", "assigned_to", "status")
|
||||
search_fields = ("id", "title")
|
||||
|
||||
def hidden_submitter_email(self, ticket):
|
||||
if ticket.submitter_email:
|
||||
@ -58,7 +63,8 @@ class TicketAdmin(admin.ModelAdmin):
|
||||
return "%s@%s" % (username, domain)
|
||||
else:
|
||||
return ticket.submitter_email
|
||||
hidden_submitter_email.short_description = _('Submitter E-Mail')
|
||||
|
||||
hidden_submitter_email.short_description = _("Submitter E-Mail")
|
||||
|
||||
def time_spent(self, ticket):
|
||||
return ticket.time_spent
|
||||
@ -82,51 +88,60 @@ class KBIAttachmentInline(admin.StackedInline):
|
||||
@admin.register(FollowUp)
|
||||
class FollowUpAdmin(admin.ModelAdmin):
|
||||
inlines = [TicketChangeInline, FollowUpAttachmentInline]
|
||||
list_display = ('ticket_get_ticket_for_url', 'title', 'date', 'ticket',
|
||||
'user', 'new_status', 'time_spent')
|
||||
list_filter = ('user', 'date', 'new_status')
|
||||
list_display = (
|
||||
"ticket_get_ticket_for_url",
|
||||
"title",
|
||||
"date",
|
||||
"ticket",
|
||||
"user",
|
||||
"new_status",
|
||||
"time_spent",
|
||||
)
|
||||
list_filter = ("user", "date", "new_status")
|
||||
|
||||
def ticket_get_ticket_for_url(self, obj):
|
||||
return obj.ticket.ticket_for_url
|
||||
ticket_get_ticket_for_url.short_description = _('Slug')
|
||||
|
||||
ticket_get_ticket_for_url.short_description = _("Slug")
|
||||
|
||||
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
|
||||
@admin.register(KBItem)
|
||||
class KBItemAdmin(admin.ModelAdmin):
|
||||
list_display = ('category', 'title', 'last_updated',
|
||||
'team', 'order', 'enabled')
|
||||
list_display = ("category", "title", "last_updated", "team", "order", "enabled")
|
||||
inlines = [KBIAttachmentInline]
|
||||
readonly_fields = ('voted_by', 'downvoted_by')
|
||||
readonly_fields = ("voted_by", "downvoted_by")
|
||||
|
||||
list_display_links = ('title',)
|
||||
list_display_links = ("title",)
|
||||
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
|
||||
@admin.register(KBCategory)
|
||||
class KBCategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'title', 'slug', 'public')
|
||||
list_display = ("name", "title", "slug", "public")
|
||||
|
||||
|
||||
@admin.register(CustomField)
|
||||
class CustomFieldAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'label', 'data_type')
|
||||
list_display = ("name", "label", "data_type")
|
||||
|
||||
|
||||
@admin.register(EmailTemplate)
|
||||
class EmailTemplateAdmin(admin.ModelAdmin):
|
||||
list_display = ('template_name', 'heading', 'locale')
|
||||
list_filter = ('locale', )
|
||||
list_display = ("template_name", "heading", "locale")
|
||||
list_filter = ("locale",)
|
||||
|
||||
|
||||
@admin.register(IgnoreEmail)
|
||||
class IgnoreEmailAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'queue_list', 'email_address', 'keep_in_mailbox')
|
||||
list_display = ("name", "queue_list", "email_address", "keep_in_mailbox")
|
||||
|
||||
|
||||
@admin.register(ChecklistTemplate)
|
||||
class ChecklistTemplateAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'task_list')
|
||||
search_fields = ('name', 'task_list')
|
||||
list_display = ("name", "task_list")
|
||||
search_fields = ("name", "task_list")
|
||||
|
||||
|
||||
class ChecklistTaskInline(admin.TabularInline):
|
||||
@ -135,10 +150,10 @@ class ChecklistTaskInline(admin.TabularInline):
|
||||
|
||||
@admin.register(Checklist)
|
||||
class ChecklistAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'ticket')
|
||||
search_fields = ('name', 'ticket__id', 'ticket__title')
|
||||
autocomplete_fields = ('ticket',)
|
||||
list_select_related = ('ticket',)
|
||||
list_display = ("name", "ticket")
|
||||
search_fields = ("name", "ticket__id", "ticket__title")
|
||||
autocomplete_fields = ("ticket",)
|
||||
list_select_related = ("ticket",)
|
||||
inlines = (ChecklistTaskInline,)
|
||||
|
||||
|
||||
|
@ -2,12 +2,12 @@ from django.apps import AppConfig
|
||||
|
||||
|
||||
class HelpdeskConfig(AppConfig):
|
||||
name = 'helpdesk'
|
||||
name = "helpdesk"
|
||||
verbose_name = "Helpdesk"
|
||||
# for Django 3.2 support:
|
||||
# see:
|
||||
# https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.default_auto_field
|
||||
default_auto_field = 'django.db.models.AutoField'
|
||||
default_auto_field = "django.db.models.AutoField"
|
||||
|
||||
def ready(self):
|
||||
from . import webhooks # noqa: F401
|
||||
from . import webhooks # noqa: F401
|
||||
|
@ -12,6 +12,7 @@ def check_staff_status(check_staff=False):
|
||||
The function most only take one User parameter at the end for use with
|
||||
the Django function user_passes_test.
|
||||
"""
|
||||
|
||||
def check_superuser_status(check_superuser):
|
||||
def check_user_status(u):
|
||||
is_ok = u.is_authenticated and u.is_active
|
||||
@ -21,7 +22,9 @@ def check_staff_status(check_staff=False):
|
||||
return is_ok and u.is_superuser
|
||||
else:
|
||||
return is_ok
|
||||
|
||||
return check_user_status
|
||||
|
||||
return check_superuser_status
|
||||
|
||||
|
||||
@ -43,11 +46,18 @@ def protect_view(view_func):
|
||||
Decorator for protecting the views checking user, redirecting
|
||||
to the log-in page if necessary or returning 404 status code
|
||||
"""
|
||||
|
||||
@wraps(view_func)
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
if not request.user.is_authenticated and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT:
|
||||
return redirect('helpdesk:login')
|
||||
elif not request.user.is_authenticated and helpdesk_settings.HELPDESK_ANON_ACCESS_RAISES_404:
|
||||
if (
|
||||
not request.user.is_authenticated
|
||||
and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT
|
||||
):
|
||||
return redirect("helpdesk:login")
|
||||
elif (
|
||||
not request.user.is_authenticated
|
||||
and helpdesk_settings.HELPDESK_ANON_ACCESS_RAISES_404
|
||||
):
|
||||
raise Http404
|
||||
if auth_redirect := helpdesk_settings.HELPDESK_PUBLIC_VIEW_PROTECTOR(request):
|
||||
return auth_redirect
|
||||
@ -61,11 +71,15 @@ def staff_member_required(view_func):
|
||||
Decorator for staff member the views checking user, redirecting
|
||||
to the log-in page if necessary or returning 403
|
||||
"""
|
||||
|
||||
@wraps(view_func)
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
if not request.user.is_authenticated and not request.user.is_active:
|
||||
return redirect('helpdesk:login')
|
||||
if not helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE and not request.user.is_staff:
|
||||
return redirect("helpdesk:login")
|
||||
if (
|
||||
not helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
||||
and not request.user.is_staff
|
||||
):
|
||||
raise PermissionDenied()
|
||||
if auth_redirect := helpdesk_settings.HELPDESK_STAFF_VIEW_PROTECTOR(request):
|
||||
return auth_redirect
|
||||
@ -79,10 +93,11 @@ def superuser_required(view_func):
|
||||
Decorator for superuser member the views checking user, redirecting
|
||||
to the log-in page if necessary or returning 403
|
||||
"""
|
||||
|
||||
@wraps(view_func)
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
if not request.user.is_authenticated and not request.user.is_active:
|
||||
return redirect('helpdesk:login')
|
||||
return redirect("helpdesk:login")
|
||||
if not request.user.is_superuser:
|
||||
raise PermissionDenied()
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@ class IgnoreTicketException(Exception):
|
||||
"""
|
||||
Raised when an email message is received from a sender who is marked to be ignored
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@ -10,4 +11,5 @@ class DeleteIgnoredTicketException(Exception):
|
||||
Raised when an email message is received from a sender who is marked to be ignored
|
||||
and the record is tagged to delete the email from the inbox
|
||||
"""
|
||||
|
||||
pass
|
||||
|
@ -27,7 +27,7 @@ from helpdesk.models import (
|
||||
TicketCC,
|
||||
TicketCustomFieldValue,
|
||||
TicketDependency,
|
||||
UserSettings
|
||||
UserSettings,
|
||||
)
|
||||
from helpdesk.settings import (
|
||||
CUSTOMFIELD_DATE_FORMAT,
|
||||
@ -55,67 +55,71 @@ class CustomFieldMixin(object):
|
||||
|
||||
def customfield_to_field(self, field, instanceargs):
|
||||
# Use TextInput widget by default
|
||||
instanceargs['widget'] = forms.TextInput(
|
||||
attrs={'class': 'form-control'})
|
||||
instanceargs["widget"] = forms.TextInput(attrs={"class": "form-control"})
|
||||
# if-elif branches start with special cases
|
||||
if field.data_type == 'varchar':
|
||||
if field.data_type == "varchar":
|
||||
fieldclass = forms.CharField
|
||||
instanceargs['max_length'] = field.max_length
|
||||
elif field.data_type == 'text':
|
||||
instanceargs["max_length"] = field.max_length
|
||||
elif field.data_type == "text":
|
||||
fieldclass = forms.CharField
|
||||
instanceargs['widget'] = forms.Textarea(
|
||||
attrs={'class': 'form-control'})
|
||||
instanceargs['max_length'] = field.max_length
|
||||
elif field.data_type == 'integer':
|
||||
instanceargs["widget"] = forms.Textarea(attrs={"class": "form-control"})
|
||||
instanceargs["max_length"] = field.max_length
|
||||
elif field.data_type == "integer":
|
||||
fieldclass = forms.IntegerField
|
||||
instanceargs['widget'] = forms.NumberInput(
|
||||
attrs={'class': 'form-control'})
|
||||
elif field.data_type == 'decimal':
|
||||
instanceargs["widget"] = forms.NumberInput(attrs={"class": "form-control"})
|
||||
elif field.data_type == "decimal":
|
||||
fieldclass = forms.DecimalField
|
||||
instanceargs['decimal_places'] = field.decimal_places
|
||||
instanceargs['max_digits'] = field.max_length
|
||||
instanceargs['widget'] = forms.NumberInput(
|
||||
attrs={'class': 'form-control'})
|
||||
elif field.data_type == 'list':
|
||||
instanceargs["decimal_places"] = field.decimal_places
|
||||
instanceargs["max_digits"] = field.max_length
|
||||
instanceargs["widget"] = forms.NumberInput(attrs={"class": "form-control"})
|
||||
elif field.data_type == "list":
|
||||
fieldclass = forms.ChoiceField
|
||||
instanceargs['choices'] = field.get_choices()
|
||||
instanceargs['widget'] = forms.Select(
|
||||
attrs={'class': 'form-control'})
|
||||
instanceargs["choices"] = field.get_choices()
|
||||
instanceargs["widget"] = forms.Select(attrs={"class": "form-control"})
|
||||
else:
|
||||
# Try to use the immediate equivalences dictionary
|
||||
try:
|
||||
fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type]
|
||||
# Change widgets for the following classes
|
||||
if fieldclass == forms.DateField:
|
||||
instanceargs['widget'] = forms.DateInput(
|
||||
attrs={'class': 'form-control date-field'})
|
||||
instanceargs["widget"] = forms.DateInput(
|
||||
attrs={"class": "form-control date-field"}
|
||||
)
|
||||
elif fieldclass == forms.DateTimeField:
|
||||
instanceargs['widget'] = forms.DateTimeInput(
|
||||
attrs={'class': 'form-control datetime-field'})
|
||||
instanceargs["widget"] = forms.DateTimeInput(
|
||||
attrs={"class": "form-control datetime-field"}
|
||||
)
|
||||
elif fieldclass == forms.TimeField:
|
||||
instanceargs['widget'] = forms.TimeInput(
|
||||
attrs={'class': 'form-control time-field'})
|
||||
instanceargs["widget"] = forms.TimeInput(
|
||||
attrs={"class": "form-control time-field"}
|
||||
)
|
||||
elif fieldclass == forms.BooleanField:
|
||||
instanceargs['widget'] = forms.CheckboxInput(
|
||||
attrs={'class': 'form-control'})
|
||||
instanceargs["widget"] = forms.CheckboxInput(
|
||||
attrs={"class": "form-control"}
|
||||
)
|
||||
|
||||
except KeyError:
|
||||
# The data_type was not found anywhere
|
||||
raise NameError("Unrecognized data_type %s" % field.data_type)
|
||||
|
||||
self.fields['custom_%s' % field.name] = fieldclass(**instanceargs)
|
||||
self.fields["custom_%s" % field.name] = fieldclass(**instanceargs)
|
||||
|
||||
|
||||
class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
exclude = ('created', 'modified', 'status', 'on_hold',
|
||||
'resolution', 'last_escalation', 'assigned_to')
|
||||
exclude = (
|
||||
"created",
|
||||
"modified",
|
||||
"status",
|
||||
"on_hold",
|
||||
"resolution",
|
||||
"last_escalation",
|
||||
"assigned_to",
|
||||
)
|
||||
|
||||
class Media:
|
||||
js = ('helpdesk/js/init_due_date.js',
|
||||
'helpdesk/js/init_datetime_classes.js')
|
||||
js = ("helpdesk/js/init_due_date.js", "helpdesk/js/init_datetime_classes.js")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
@ -124,56 +128,62 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
||||
super(EditTicketForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Disable and add help_text to the merged_to field on this form
|
||||
self.fields['merged_to'].disabled = True
|
||||
self.fields['merged_to'].help_text = _(
|
||||
'This ticket is merged into the selected ticket.')
|
||||
self.fields["merged_to"].disabled = True
|
||||
self.fields["merged_to"].help_text = _(
|
||||
"This ticket is merged into the selected ticket."
|
||||
)
|
||||
|
||||
for field in CustomField.objects.all():
|
||||
initial_value = None
|
||||
try:
|
||||
current_value = TicketCustomFieldValue.objects.get(
|
||||
ticket=self.instance, field=field)
|
||||
ticket=self.instance, field=field
|
||||
)
|
||||
initial_value = current_value.value
|
||||
# Attempt to convert from fixed format string to date/time data
|
||||
# type
|
||||
if 'datetime' == current_value.field.data_type:
|
||||
if "datetime" == current_value.field.data_type:
|
||||
initial_value = datetime.strptime(
|
||||
initial_value, CUSTOMFIELD_DATETIME_FORMAT)
|
||||
elif 'date' == current_value.field.data_type:
|
||||
initial_value, CUSTOMFIELD_DATETIME_FORMAT
|
||||
)
|
||||
elif "date" == current_value.field.data_type:
|
||||
initial_value = datetime.strptime(
|
||||
initial_value, CUSTOMFIELD_DATE_FORMAT)
|
||||
elif 'time' == current_value.field.data_type:
|
||||
initial_value, CUSTOMFIELD_DATE_FORMAT
|
||||
)
|
||||
elif "time" == current_value.field.data_type:
|
||||
initial_value = datetime.strptime(
|
||||
initial_value, CUSTOMFIELD_TIME_FORMAT)
|
||||
initial_value, CUSTOMFIELD_TIME_FORMAT
|
||||
)
|
||||
# If it is boolean field, transform the value to a real boolean
|
||||
# instead of a string
|
||||
elif 'boolean' == current_value.field.data_type:
|
||||
initial_value = 'True' == initial_value
|
||||
elif "boolean" == current_value.field.data_type:
|
||||
initial_value = "True" == initial_value
|
||||
except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError):
|
||||
# ValueError error if parsing fails, using initial_value = current_value.value
|
||||
# TypeError if parsing None type
|
||||
pass
|
||||
instanceargs = {
|
||||
'label': field.label,
|
||||
'help_text': field.help_text,
|
||||
'required': field.required,
|
||||
'initial': initial_value,
|
||||
"label": field.label,
|
||||
"help_text": field.help_text,
|
||||
"required": field.required,
|
||||
"initial": initial_value,
|
||||
}
|
||||
|
||||
self.customfield_to_field(field, instanceargs)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
for field, value in self.cleaned_data.items():
|
||||
if field.startswith('custom_'):
|
||||
field_name = field.replace('custom_', '', 1)
|
||||
if field.startswith("custom_"):
|
||||
field_name = field.replace("custom_", "", 1)
|
||||
customfield = CustomField.objects.get(name=field_name)
|
||||
try:
|
||||
cfv = TicketCustomFieldValue.objects.get(
|
||||
ticket=self.instance, field=customfield)
|
||||
ticket=self.instance, field=customfield
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
cfv = TicketCustomFieldValue(
|
||||
ticket=self.instance, field=customfield)
|
||||
ticket=self.instance, field=customfield
|
||||
)
|
||||
|
||||
cfv.value = convert_value(value)
|
||||
cfv.save()
|
||||
@ -195,21 +205,24 @@ class EditTicketCustomFieldForm(EditTicketForm):
|
||||
if HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST:
|
||||
fields = list(self.fields)
|
||||
for field in fields:
|
||||
if field != 'id' and field.replace("custom_", "", 1) not in HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST:
|
||||
if (
|
||||
field != "id"
|
||||
and field.replace("custom_", "", 1)
|
||||
not in HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST
|
||||
):
|
||||
self.fields.pop(field, None)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# if form is saved in a ticket update, it is passed
|
||||
# a followup instance to trace custom fields changes
|
||||
if "followup" in kwargs:
|
||||
followup = kwargs.pop('followup', None)
|
||||
followup = kwargs.pop("followup", None)
|
||||
|
||||
for field, value in self.cleaned_data.items():
|
||||
if field.startswith('custom_'):
|
||||
if field.startswith("custom_"):
|
||||
if value != self.fields[field].initial:
|
||||
c = followup.ticketchange_set.create(
|
||||
field=field.replace('custom_', '', 1),
|
||||
field=field.replace("custom_", "", 1),
|
||||
old_value=self.fields[field].initial,
|
||||
new_value=value,
|
||||
)
|
||||
@ -218,20 +231,26 @@ class EditTicketCustomFieldForm(EditTicketForm):
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = ('id', 'merged_to',)
|
||||
fields = (
|
||||
"id",
|
||||
"merged_to",
|
||||
)
|
||||
|
||||
|
||||
class EditFollowUpForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = FollowUp
|
||||
exclude = ('date', 'user',)
|
||||
exclude = (
|
||||
"date",
|
||||
"user",
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Filter not opened tickets here."""
|
||||
super(EditFollowUpForm, self).__init__(*args, **kwargs)
|
||||
self.fields["ticket"].queryset = Ticket.objects.filter(
|
||||
status__in=Ticket.OPEN_STATUSES)
|
||||
status__in=Ticket.OPEN_STATUSES
|
||||
)
|
||||
|
||||
|
||||
class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
@ -239,73 +258,81 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
Contain all the common code and fields between "TicketForm" and
|
||||
"PublicTicketForm". This Form is not intended to be used directly.
|
||||
"""
|
||||
|
||||
queue = forms.ChoiceField(
|
||||
widget=forms.Select(attrs={'class': 'form-control'}),
|
||||
label=_('Queue'),
|
||||
widget=forms.Select(attrs={"class": "form-control"}),
|
||||
label=_("Queue"),
|
||||
required=True,
|
||||
choices=()
|
||||
choices=(),
|
||||
)
|
||||
|
||||
title = forms.CharField(
|
||||
max_length=100,
|
||||
required=True,
|
||||
widget=forms.TextInput(attrs={'class': 'form-control'}),
|
||||
label=_('Summary of the problem'),
|
||||
widget=forms.TextInput(attrs={"class": "form-control"}),
|
||||
label=_("Summary of the problem"),
|
||||
)
|
||||
|
||||
body = forms.CharField(
|
||||
widget=forms.Textarea(attrs={'class': 'form-control'}),
|
||||
label=_('Description of your issue'),
|
||||
widget=forms.Textarea(attrs={"class": "form-control"}),
|
||||
label=_("Description of your issue"),
|
||||
required=True,
|
||||
help_text=_(
|
||||
'Please be as descriptive as possible and include all details'),
|
||||
help_text=_("Please be as descriptive as possible and include all details"),
|
||||
)
|
||||
|
||||
priority = forms.ChoiceField(
|
||||
widget=forms.Select(attrs={'class': 'form-control'}),
|
||||
widget=forms.Select(attrs={"class": "form-control"}),
|
||||
choices=Ticket.PRIORITY_CHOICES,
|
||||
required=True,
|
||||
initial=getattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY', '3'),
|
||||
label=_('Priority'),
|
||||
help_text=_(
|
||||
"Please select a priority carefully. If unsure, leave it as '3'."),
|
||||
initial=getattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY", "3"),
|
||||
label=_("Priority"),
|
||||
help_text=_("Please select a priority carefully. If unsure, leave it as '3'."),
|
||||
)
|
||||
|
||||
due_date = forms.DateTimeField(
|
||||
widget=forms.TextInput(
|
||||
attrs={'class': 'form-control', 'autocomplete': 'off'}),
|
||||
widget=forms.TextInput(attrs={"class": "form-control", "autocomplete": "off"}),
|
||||
required=False,
|
||||
input_formats=[CUSTOMFIELD_DATE_FORMAT,
|
||||
CUSTOMFIELD_DATETIME_FORMAT, '%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"],
|
||||
label=_('Due on'),
|
||||
input_formats=[
|
||||
CUSTOMFIELD_DATE_FORMAT,
|
||||
CUSTOMFIELD_DATETIME_FORMAT,
|
||||
"%d/%m/%Y",
|
||||
"%m/%d/%Y",
|
||||
"%d.%m.%Y",
|
||||
],
|
||||
label=_("Due on"),
|
||||
)
|
||||
|
||||
if helpdesk_settings.HELPDESK_ENABLE_ATTACHMENTS:
|
||||
attachment = forms.FileField(
|
||||
widget=forms.FileInput(attrs={'class': 'form-control-file'}),
|
||||
widget=forms.FileInput(attrs={"class": "form-control-file"}),
|
||||
required=False,
|
||||
label=_('Attach File'),
|
||||
help_text=_('You can attach a file to this ticket. '
|
||||
'Only file types such as plain text (.txt), '
|
||||
'a document (.pdf, .docx, or .odt), '
|
||||
'or screenshot (.png or .jpg) may be uploaded.'),
|
||||
validators=[validate_file_extension]
|
||||
label=_("Attach File"),
|
||||
help_text=_(
|
||||
"You can attach a file to this ticket. "
|
||||
"Only file types such as plain text (.txt), "
|
||||
"a document (.pdf, .docx, or .odt), "
|
||||
"or screenshot (.png or .jpg) may be uploaded."
|
||||
),
|
||||
validators=[validate_file_extension],
|
||||
)
|
||||
|
||||
|
||||
class Media:
|
||||
js = ('helpdesk/js/init_due_date.js',
|
||||
'helpdesk/js/init_datetime_classes.js')
|
||||
js = ("helpdesk/js/init_due_date.js", "helpdesk/js/init_datetime_classes.js")
|
||||
|
||||
def __init__(self, kbcategory=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
if kbcategory:
|
||||
self.fields['kbitem'] = forms.ChoiceField(
|
||||
widget=forms.Select(attrs={'class': 'form-control'}),
|
||||
self.fields["kbitem"] = forms.ChoiceField(
|
||||
widget=forms.Select(attrs={"class": "form-control"}),
|
||||
required=False,
|
||||
label=_('Knowledge Base Item'),
|
||||
choices=[(kbi.pk, kbi.title) for kbi in KBItem.objects.filter(
|
||||
category=kbcategory.pk, enabled=True)],
|
||||
label=_("Knowledge Base Item"),
|
||||
choices=[
|
||||
(kbi.pk, kbi.title)
|
||||
for kbi in KBItem.objects.filter(
|
||||
category=kbcategory.pk, enabled=True
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def _add_form_custom_fields(self, staff_only_filter=None):
|
||||
@ -316,38 +343,37 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
|
||||
for field in queryset:
|
||||
instanceargs = {
|
||||
'label': field.label,
|
||||
'help_text': field.help_text,
|
||||
'required': field.required,
|
||||
"label": field.label,
|
||||
"help_text": field.help_text,
|
||||
"required": field.required,
|
||||
}
|
||||
|
||||
self.customfield_to_field(field, instanceargs)
|
||||
|
||||
def _get_queue(self):
|
||||
# this procedure is re-defined for public submission form
|
||||
return Queue.objects.get(id=int(self.cleaned_data['queue']))
|
||||
return Queue.objects.get(id=int(self.cleaned_data["queue"]))
|
||||
|
||||
def _create_ticket(self):
|
||||
queue = self._get_queue()
|
||||
kbitem = None
|
||||
if 'kbitem' in self.cleaned_data:
|
||||
kbitem = KBItem.objects.get(id=int(self.cleaned_data['kbitem']))
|
||||
if "kbitem" in self.cleaned_data:
|
||||
kbitem = KBItem.objects.get(id=int(self.cleaned_data["kbitem"]))
|
||||
|
||||
ticket = Ticket(
|
||||
title=self.cleaned_data['title'],
|
||||
submitter_email=self.cleaned_data['submitter_email'],
|
||||
title=self.cleaned_data["title"],
|
||||
submitter_email=self.cleaned_data["submitter_email"],
|
||||
created=timezone.now(),
|
||||
status=Ticket.OPEN_STATUS,
|
||||
queue=queue,
|
||||
description=self.cleaned_data['body'],
|
||||
description=self.cleaned_data["body"],
|
||||
priority=self.cleaned_data.get(
|
||||
'priority',
|
||||
getattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY", "3")
|
||||
"priority", getattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY", "3")
|
||||
),
|
||||
due_date=self.cleaned_data.get(
|
||||
'due_date',
|
||||
getattr(settings, "HELPDESK_PUBLIC_TICKET_DUE_DATE", None)
|
||||
) or None,
|
||||
"due_date", getattr(settings, "HELPDESK_PUBLIC_TICKET_DUE_DATE", None)
|
||||
)
|
||||
or None,
|
||||
kbitem=kbitem,
|
||||
)
|
||||
|
||||
@ -357,18 +383,19 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
ticket.save_custom_field_values(self.cleaned_data)
|
||||
|
||||
def _create_follow_up(self, ticket, title, user=None):
|
||||
followup = FollowUp(ticket=ticket,
|
||||
title=title,
|
||||
date=timezone.now(),
|
||||
public=True,
|
||||
comment=self.cleaned_data['body'],
|
||||
)
|
||||
followup = FollowUp(
|
||||
ticket=ticket,
|
||||
title=title,
|
||||
date=timezone.now(),
|
||||
public=True,
|
||||
comment=self.cleaned_data["body"],
|
||||
)
|
||||
if user:
|
||||
followup.user = user
|
||||
return followup
|
||||
|
||||
def _attach_files_to_follow_up(self, followup):
|
||||
files = self.cleaned_data.get('attachment')
|
||||
files = self.cleaned_data.get("attachment")
|
||||
if files:
|
||||
files = process_attachments(followup, [files])
|
||||
return files
|
||||
@ -376,13 +403,18 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
@staticmethod
|
||||
def _send_messages(ticket, queue, followup, files, user=None):
|
||||
context = safe_template_context(ticket)
|
||||
context['comment'] = followup.comment
|
||||
context["comment"] = followup.comment
|
||||
|
||||
roles = {'submitter': ('newticket_submitter', context),
|
||||
'new_ticket_cc': ('newticket_cc', context),
|
||||
'ticket_cc': ('newticket_cc', context)}
|
||||
if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign:
|
||||
roles['assigned_to'] = ('assigned_owner', context)
|
||||
roles = {
|
||||
"submitter": ("newticket_submitter", context),
|
||||
"new_ticket_cc": ("newticket_cc", context),
|
||||
"ticket_cc": ("newticket_cc", context),
|
||||
}
|
||||
if (
|
||||
ticket.assigned_to
|
||||
and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign
|
||||
):
|
||||
roles["assigned_to"] = ("assigned_owner", context)
|
||||
ticket.send(
|
||||
roles,
|
||||
fail_silently=True,
|
||||
@ -394,26 +426,29 @@ class TicketForm(AbstractTicketForm):
|
||||
"""
|
||||
Ticket Form creation for registered users.
|
||||
"""
|
||||
|
||||
submitter_email = forms.EmailField(
|
||||
required=False,
|
||||
label=_('Submitter E-Mail Address'),
|
||||
widget=forms.TextInput(
|
||||
attrs={'class': 'form-control', 'type': 'email'}),
|
||||
help_text=_('This e-mail address will receive copies of all public '
|
||||
'updates to this ticket.'),
|
||||
label=_("Submitter E-Mail Address"),
|
||||
widget=forms.TextInput(attrs={"class": "form-control", "type": "email"}),
|
||||
help_text=_(
|
||||
"This e-mail address will receive copies of all public "
|
||||
"updates to this ticket."
|
||||
),
|
||||
)
|
||||
assigned_to = forms.ChoiceField(
|
||||
widget=(
|
||||
forms.Select(attrs={'class': 'form-control'})
|
||||
forms.Select(attrs={"class": "form-control"})
|
||||
if not helpdesk_settings.HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO
|
||||
else forms.HiddenInput()
|
||||
),
|
||||
required=False,
|
||||
label=_('Case owner'),
|
||||
help_text=_('If you select an owner other than yourself, they\'ll be '
|
||||
'e-mailed details of this ticket immediately.'),
|
||||
|
||||
choices=()
|
||||
label=_("Case owner"),
|
||||
help_text=_(
|
||||
"If you select an owner other than yourself, they'll be "
|
||||
"e-mailed details of this ticket immediately."
|
||||
),
|
||||
choices=(),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -424,15 +459,18 @@ class TicketForm(AbstractTicketForm):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['queue'].choices = queue_choices
|
||||
self.fields["queue"].choices = queue_choices
|
||||
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS:
|
||||
assignable_users = User.objects.filter(
|
||||
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
|
||||
is_active=True, is_staff=True
|
||||
).order_by(User.USERNAME_FIELD)
|
||||
else:
|
||||
assignable_users = User.objects.filter(
|
||||
is_active=True).order_by(User.USERNAME_FIELD)
|
||||
self.fields['assigned_to'].choices = [
|
||||
('', '--------')] + [(u.id, u.get_username()) for u in assignable_users]
|
||||
assignable_users = User.objects.filter(is_active=True).order_by(
|
||||
User.USERNAME_FIELD
|
||||
)
|
||||
self.fields["assigned_to"].choices = [("", "--------")] + [
|
||||
(u.id, u.get_username()) for u in assignable_users
|
||||
]
|
||||
self._add_form_custom_fields()
|
||||
|
||||
def save(self, user):
|
||||
@ -441,9 +479,9 @@ class TicketForm(AbstractTicketForm):
|
||||
"""
|
||||
|
||||
ticket, queue = self._create_ticket()
|
||||
if self.cleaned_data['assigned_to']:
|
||||
if self.cleaned_data["assigned_to"]:
|
||||
try:
|
||||
u = User.objects.get(id=self.cleaned_data['assigned_to'])
|
||||
u = User.objects.get(id=self.cleaned_data["assigned_to"])
|
||||
ticket.assigned_to = u
|
||||
except User.DoesNotExist:
|
||||
ticket.assigned_to = None
|
||||
@ -451,12 +489,12 @@ class TicketForm(AbstractTicketForm):
|
||||
|
||||
self._create_custom_fields(ticket)
|
||||
|
||||
if self.cleaned_data['assigned_to']:
|
||||
title = _('Ticket Opened & Assigned to %(name)s') % {
|
||||
'name': ticket.get_assigned_to or _("<invalid user>")
|
||||
if self.cleaned_data["assigned_to"]:
|
||||
title = _("Ticket Opened & Assigned to %(name)s") % {
|
||||
"name": ticket.get_assigned_to or _("<invalid user>")
|
||||
}
|
||||
else:
|
||||
title = _('Ticket Opened')
|
||||
title = _("Ticket Opened")
|
||||
followup = self._create_follow_up(ticket, title=title, user=user)
|
||||
followup.save()
|
||||
|
||||
@ -468,11 +506,9 @@ class TicketForm(AbstractTicketForm):
|
||||
# emit signal when the TicketForm.save is done
|
||||
new_ticket_done.send(sender="TicketForm", ticket=ticket)
|
||||
|
||||
self._send_messages(ticket=ticket,
|
||||
queue=queue,
|
||||
followup=followup,
|
||||
files=files,
|
||||
user=user)
|
||||
self._send_messages(
|
||||
ticket=ticket, queue=queue, followup=followup, files=files, user=user
|
||||
)
|
||||
return ticket
|
||||
|
||||
|
||||
@ -480,12 +516,12 @@ class PublicTicketForm(AbstractTicketForm):
|
||||
"""
|
||||
Ticket Form creation for all users (public-facing).
|
||||
"""
|
||||
|
||||
submitter_email = forms.EmailField(
|
||||
widget=forms.TextInput(
|
||||
attrs={'class': 'form-control', 'type': 'email'}),
|
||||
widget=forms.TextInput(attrs={"class": "form-control", "type": "email"}),
|
||||
required=True,
|
||||
label=_('Your E-Mail Address'),
|
||||
help_text=_('We will e-mail you when your ticket is updated.'),
|
||||
label=_("Your E-Mail Address"),
|
||||
help_text=_("We will e-mail you when your ticket is updated."),
|
||||
)
|
||||
|
||||
def __init__(self, hidden_fields=(), readonly_fields=(), *args, **kwargs):
|
||||
@ -502,14 +538,13 @@ class PublicTicketForm(AbstractTicketForm):
|
||||
self.fields[field].disabled = True
|
||||
|
||||
field_deletion_table = {
|
||||
'queue': 'HELPDESK_PUBLIC_TICKET_QUEUE',
|
||||
'priority': 'HELPDESK_PUBLIC_TICKET_PRIORITY',
|
||||
'due_date': 'HELPDESK_PUBLIC_TICKET_DUE_DATE',
|
||||
"queue": "HELPDESK_PUBLIC_TICKET_QUEUE",
|
||||
"priority": "HELPDESK_PUBLIC_TICKET_PRIORITY",
|
||||
"due_date": "HELPDESK_PUBLIC_TICKET_DUE_DATE",
|
||||
}
|
||||
|
||||
for field_name, field_setting_key in field_deletion_table.items():
|
||||
has_settings_default_value = getattr(
|
||||
settings, field_setting_key, None)
|
||||
has_settings_default_value = getattr(settings, field_setting_key, None)
|
||||
if has_settings_default_value is not None:
|
||||
del self.fields[field_name]
|
||||
|
||||
@ -520,12 +555,13 @@ class PublicTicketForm(AbstractTicketForm):
|
||||
"There are no public queues defined - public ticket creation is impossible"
|
||||
)
|
||||
|
||||
if 'queue' in self.fields:
|
||||
self.fields['queue'].choices = [('', '--------')] + [
|
||||
(q.id, q.title) for q in public_queues]
|
||||
if "queue" in self.fields:
|
||||
self.fields["queue"].choices = [("", "--------")] + [
|
||||
(q.id, q.title) for q in public_queues
|
||||
]
|
||||
|
||||
def _get_queue(self):
|
||||
if getattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE', None) is not None:
|
||||
if getattr(settings, "HELPDESK_PUBLIC_TICKET_QUEUE", None) is not None:
|
||||
# force queue to be the pre-defined one
|
||||
# (only for public submissions)
|
||||
public_queue = Queue.objects.filter(
|
||||
@ -534,12 +570,12 @@ class PublicTicketForm(AbstractTicketForm):
|
||||
if not public_queue:
|
||||
logger.fatal(
|
||||
"Public queue '%s' is configured as default but can't be found",
|
||||
settings.HELPDESK_PUBLIC_TICKET_QUEUE
|
||||
settings.HELPDESK_PUBLIC_TICKET_QUEUE,
|
||||
)
|
||||
return public_queue
|
||||
else:
|
||||
# get the queue user entered
|
||||
return Queue.objects.get(id=int(self.cleaned_data['queue']))
|
||||
return Queue.objects.get(id=int(self.cleaned_data["queue"]))
|
||||
|
||||
def save(self, user):
|
||||
"""
|
||||
@ -553,7 +589,8 @@ class PublicTicketForm(AbstractTicketForm):
|
||||
self._create_custom_fields(ticket)
|
||||
|
||||
followup = self._create_follow_up(
|
||||
ticket, title=_('Ticket Opened Via Web'), user=user)
|
||||
ticket, title=_("Ticket Opened Via Web"), user=user
|
||||
)
|
||||
followup.save()
|
||||
|
||||
files = self._attach_files_to_follow_up(followup)
|
||||
@ -561,161 +598,174 @@ class PublicTicketForm(AbstractTicketForm):
|
||||
# emit signal when the PublicTicketForm.save is done
|
||||
new_ticket_done.send(sender="PublicTicketForm", ticket=ticket)
|
||||
|
||||
self._send_messages(ticket=ticket,
|
||||
queue=queue,
|
||||
followup=followup,
|
||||
files=files)
|
||||
self._send_messages(ticket=ticket, queue=queue, followup=followup, files=files)
|
||||
return ticket
|
||||
|
||||
|
||||
class UserSettingsForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = UserSettings
|
||||
exclude = ['user', 'settings_pickled']
|
||||
exclude = ["user", "settings_pickled"]
|
||||
|
||||
|
||||
class EmailIgnoreForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = IgnoreEmail
|
||||
exclude = []
|
||||
|
||||
|
||||
class TicketCCForm(forms.ModelForm):
|
||||
''' Adds either an email address or helpdesk user as a CC on a Ticket. Used for processing POST requests. '''
|
||||
"""Adds either an email address or helpdesk user as a CC on a Ticket. Used for processing POST requests."""
|
||||
|
||||
class Meta:
|
||||
model = TicketCC
|
||||
exclude = ('ticket',)
|
||||
exclude = ("ticket",)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TicketCCForm, self).__init__(*args, **kwargs)
|
||||
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
|
||||
users = User.objects.filter(
|
||||
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
|
||||
users = User.objects.filter(is_active=True, is_staff=True).order_by(
|
||||
User.USERNAME_FIELD
|
||||
)
|
||||
else:
|
||||
users = User.objects.filter(
|
||||
is_active=True).order_by(User.USERNAME_FIELD)
|
||||
self.fields['user'].queryset = users
|
||||
users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD)
|
||||
self.fields["user"].queryset = users
|
||||
|
||||
|
||||
class TicketCCUserForm(forms.ModelForm):
|
||||
''' Adds a helpdesk user as a CC on a Ticket '''
|
||||
"""Adds a helpdesk user as a CC on a Ticket"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TicketCCUserForm, self).__init__(*args, **kwargs)
|
||||
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
|
||||
users = User.objects.filter(
|
||||
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
|
||||
users = User.objects.filter(is_active=True, is_staff=True).order_by(
|
||||
User.USERNAME_FIELD
|
||||
)
|
||||
else:
|
||||
users = User.objects.filter(
|
||||
is_active=True).order_by(User.USERNAME_FIELD)
|
||||
self.fields['user'].queryset = users
|
||||
users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD)
|
||||
self.fields["user"].queryset = users
|
||||
|
||||
class Meta:
|
||||
model = TicketCC
|
||||
exclude = ('ticket', 'email',)
|
||||
exclude = (
|
||||
"ticket",
|
||||
"email",
|
||||
)
|
||||
|
||||
|
||||
class TicketCCEmailForm(forms.ModelForm):
|
||||
''' Adds an email address as a CC on a Ticket '''
|
||||
"""Adds an email address as a CC on a Ticket"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TicketCCEmailForm, self).__init__(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
model = TicketCC
|
||||
exclude = ('ticket', 'user',)
|
||||
exclude = (
|
||||
"ticket",
|
||||
"user",
|
||||
)
|
||||
|
||||
|
||||
class TicketDependencyForm(forms.ModelForm):
|
||||
''' Adds a different ticket as a dependency for this Ticket '''
|
||||
"""Adds a different ticket as a dependency for this Ticket"""
|
||||
|
||||
class Meta:
|
||||
model = TicketDependency
|
||||
fields = ('depends_on',)
|
||||
fields = ("depends_on",)
|
||||
|
||||
def __init__(self, ticket, *args, **kwargs):
|
||||
super(TicketDependencyForm,self).__init__(*args, **kwargs)
|
||||
super(TicketDependencyForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Only open tickets except myself, existing dependencies and parents
|
||||
self.fields['depends_on'].queryset = Ticket.objects.filter(status__in=Ticket.OPEN_STATUSES).exclude(id=ticket.id).exclude(depends_on__ticket=ticket).exclude(ticketdependency__depends_on=ticket)
|
||||
self.fields["depends_on"].queryset = (
|
||||
Ticket.objects.filter(status__in=Ticket.OPEN_STATUSES)
|
||||
.exclude(id=ticket.id)
|
||||
.exclude(depends_on__ticket=ticket)
|
||||
.exclude(ticketdependency__depends_on=ticket)
|
||||
)
|
||||
|
||||
|
||||
class TicketResolvesForm(forms.ModelForm):
|
||||
''' Adds this ticket as a dependency for a different ticket '''
|
||||
"""Adds this ticket as a dependency for a different ticket"""
|
||||
|
||||
class Meta:
|
||||
model = TicketDependency
|
||||
fields = ('ticket',)
|
||||
fields = ("ticket",)
|
||||
|
||||
def __init__(self, ticket, *args, **kwargs):
|
||||
super(TicketResolvesForm,self).__init__(*args, **kwargs)
|
||||
super(TicketResolvesForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Only open tickets except myself, existing dependencies and parents
|
||||
self.fields['ticket'].queryset = Ticket.objects.exclude(status__in=Ticket.OPEN_STATUSES).exclude(id=ticket.id).exclude(depends_on__ticket=ticket).exclude(ticketdependency__depends_on=ticket)
|
||||
self.fields["ticket"].queryset = (
|
||||
Ticket.objects.exclude(status__in=Ticket.OPEN_STATUSES)
|
||||
.exclude(id=ticket.id)
|
||||
.exclude(depends_on__ticket=ticket)
|
||||
.exclude(ticketdependency__depends_on=ticket)
|
||||
)
|
||||
|
||||
|
||||
class MultipleTicketSelectForm(forms.Form):
|
||||
tickets = forms.ModelMultipleChoiceField(
|
||||
label=_('Tickets to merge'),
|
||||
label=_("Tickets to merge"),
|
||||
queryset=Ticket.objects.filter(merged_to=None),
|
||||
widget=forms.SelectMultiple(attrs={'class': 'form-control'})
|
||||
widget=forms.SelectMultiple(attrs={"class": "form-control"}),
|
||||
)
|
||||
|
||||
def clean_tickets(self):
|
||||
tickets = self.cleaned_data.get('tickets')
|
||||
tickets = self.cleaned_data.get("tickets")
|
||||
if len(tickets) < 2:
|
||||
raise ValidationError(_('Please choose at least 2 tickets.'))
|
||||
raise ValidationError(_("Please choose at least 2 tickets."))
|
||||
if len(tickets) > 4:
|
||||
raise ValidationError(
|
||||
_('Impossible to merge more than 4 tickets...'))
|
||||
queues = tickets.order_by('queue').distinct(
|
||||
).values_list('queue', flat=True)
|
||||
raise ValidationError(_("Impossible to merge more than 4 tickets..."))
|
||||
queues = tickets.order_by("queue").distinct().values_list("queue", flat=True)
|
||||
if len(queues) != 1:
|
||||
raise ValidationError(
|
||||
_('All selected tickets must share the same queue in order to be merged.'))
|
||||
_(
|
||||
"All selected tickets must share the same queue in order to be merged."
|
||||
)
|
||||
)
|
||||
return tickets
|
||||
|
||||
|
||||
class ChecklistTemplateForm(forms.ModelForm):
|
||||
name = forms.CharField(
|
||||
widget=forms.TextInput(attrs={'class': 'form-control'}),
|
||||
widget=forms.TextInput(attrs={"class": "form-control"}),
|
||||
required=True,
|
||||
)
|
||||
task_list = forms.JSONField(widget=forms.HiddenInput())
|
||||
|
||||
class Meta:
|
||||
model = ChecklistTemplate
|
||||
fields = ('name', 'task_list')
|
||||
fields = ("name", "task_list")
|
||||
|
||||
def clean_task_list(self):
|
||||
task_list = self.cleaned_data['task_list']
|
||||
task_list = self.cleaned_data["task_list"]
|
||||
return list(map(lambda task: task.strip(), task_list))
|
||||
|
||||
|
||||
class ChecklistForm(forms.ModelForm):
|
||||
name = forms.CharField(
|
||||
widget=forms.TextInput(attrs={'class': 'form-control'}),
|
||||
widget=forms.TextInput(attrs={"class": "form-control"}),
|
||||
required=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Checklist
|
||||
fields = ('name',)
|
||||
fields = ("name",)
|
||||
|
||||
|
||||
class CreateChecklistForm(ChecklistForm):
|
||||
checklist_template = forms.ModelChoiceField(
|
||||
label=_("Template"),
|
||||
queryset=ChecklistTemplate.objects.all(),
|
||||
widget=forms.Select(attrs={'class': 'form-control'}),
|
||||
widget=forms.Select(attrs={"class": "form-control"}),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta(ChecklistForm.Meta):
|
||||
fields = ('checklist_template', 'name')
|
||||
fields = ("checklist_template", "name")
|
||||
|
||||
|
||||
class FormControlDeleteFormSet(forms.BaseInlineFormSet):
|
||||
deletion_widget = forms.CheckboxInput(attrs={'class': 'form-control'})
|
||||
deletion_widget = forms.CheckboxInput(attrs={"class": "form-control"})
|
||||
|
145
helpdesk/lib.py
145
helpdesk/lib.py
@ -6,34 +6,52 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
lib.py - Common functions (eg multipart e-mail)
|
||||
"""
|
||||
|
||||
|
||||
from datetime import date, datetime, time
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError, ImproperlyConfigured
|
||||
from django.utils.encoding import smart_str
|
||||
from helpdesk.settings import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT
|
||||
from helpdesk.settings import (
|
||||
CUSTOMFIELD_DATE_FORMAT,
|
||||
CUSTOMFIELD_DATETIME_FORMAT,
|
||||
CUSTOMFIELD_TIME_FORMAT,
|
||||
)
|
||||
import logging
|
||||
import mimetypes
|
||||
|
||||
|
||||
logger = logging.getLogger('helpdesk')
|
||||
logger = logging.getLogger("helpdesk")
|
||||
|
||||
|
||||
def ticket_template_context(ticket):
|
||||
context = {}
|
||||
|
||||
for field in ('title', 'created', 'modified', 'submitter_email',
|
||||
'status', 'get_status_display', 'on_hold', 'description',
|
||||
'resolution', 'priority', 'get_priority_display',
|
||||
'last_escalation', 'ticket', 'ticket_for_url', 'merged_to',
|
||||
'get_status', 'ticket_url', 'staff_url', '_get_assigned_to'
|
||||
):
|
||||
for field in (
|
||||
"title",
|
||||
"created",
|
||||
"modified",
|
||||
"submitter_email",
|
||||
"status",
|
||||
"get_status_display",
|
||||
"on_hold",
|
||||
"description",
|
||||
"resolution",
|
||||
"priority",
|
||||
"get_priority_display",
|
||||
"last_escalation",
|
||||
"ticket",
|
||||
"ticket_for_url",
|
||||
"merged_to",
|
||||
"get_status",
|
||||
"ticket_url",
|
||||
"staff_url",
|
||||
"_get_assigned_to",
|
||||
):
|
||||
attr = getattr(ticket, field, None)
|
||||
if callable(attr):
|
||||
context[field] = '%s' % attr()
|
||||
context[field] = "%s" % attr()
|
||||
else:
|
||||
context[field] = attr
|
||||
context['assigned_to'] = context['_get_assigned_to']
|
||||
context["assigned_to"] = context["_get_assigned_to"]
|
||||
|
||||
return context
|
||||
|
||||
@ -41,7 +59,7 @@ def ticket_template_context(ticket):
|
||||
def queue_template_context(queue):
|
||||
context = {}
|
||||
|
||||
for field in ('title', 'slug', 'email_address', 'from_address', 'locale'):
|
||||
for field in ("title", "slug", "email_address", "from_address", "locale"):
|
||||
attr = getattr(queue, field, None)
|
||||
if callable(attr):
|
||||
context[field] = attr()
|
||||
@ -67,10 +85,10 @@ def safe_template_context(ticket):
|
||||
"""
|
||||
|
||||
context = {
|
||||
'queue': queue_template_context(ticket.queue),
|
||||
'ticket': ticket_template_context(ticket),
|
||||
"queue": queue_template_context(ticket.queue),
|
||||
"ticket": ticket_template_context(ticket),
|
||||
}
|
||||
context['ticket']['queue'] = context['queue']
|
||||
context["ticket"]["queue"] = context["queue"]
|
||||
|
||||
return context
|
||||
|
||||
@ -87,41 +105,42 @@ def text_is_spam(text, request):
|
||||
return False
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
try:
|
||||
site = Site.objects.get_current()
|
||||
except ImproperlyConfigured:
|
||||
site = Site(domain='configure-django-sites.com')
|
||||
site = Site(domain="configure-django-sites.com")
|
||||
|
||||
# see https://akismet.readthedocs.io/en/latest/overview.html#using-akismet
|
||||
|
||||
apikey = None
|
||||
|
||||
if hasattr(settings, 'TYPEPAD_ANTISPAM_API_KEY'):
|
||||
if hasattr(settings, "TYPEPAD_ANTISPAM_API_KEY"):
|
||||
apikey = settings.TYPEPAD_ANTISPAM_API_KEY
|
||||
elif hasattr(settings, 'PYTHON_AKISMET_API_KEY'):
|
||||
elif hasattr(settings, "PYTHON_AKISMET_API_KEY"):
|
||||
# new env var expected by python-akismet package
|
||||
apikey = settings.PYTHON_AKISMET_API_KEY
|
||||
elif hasattr(settings, 'AKISMET_API_KEY'):
|
||||
elif hasattr(settings, "AKISMET_API_KEY"):
|
||||
# deprecated, but kept for backward compatibility
|
||||
apikey = settings.AKISMET_API_KEY
|
||||
else:
|
||||
return False
|
||||
|
||||
ak = Akismet(
|
||||
blog_url='http://%s/' % site.domain,
|
||||
blog_url="http://%s/" % site.domain,
|
||||
key=apikey,
|
||||
)
|
||||
|
||||
if hasattr(settings, 'TYPEPAD_ANTISPAM_API_KEY'):
|
||||
ak.baseurl = 'api.antispam.typepad.com/1.1/'
|
||||
if hasattr(settings, "TYPEPAD_ANTISPAM_API_KEY"):
|
||||
ak.baseurl = "api.antispam.typepad.com/1.1/"
|
||||
|
||||
if ak.verify_key():
|
||||
ak_data = {
|
||||
'user_ip': request.META.get('REMOTE_ADDR', '127.0.0.1'),
|
||||
'user_agent': request.headers.get('User-Agent', ''),
|
||||
'referrer': request.headers.get('Referer', ''),
|
||||
'comment_type': 'comment',
|
||||
'comment_author': '',
|
||||
"user_ip": request.META.get("REMOTE_ADDR", "127.0.0.1"),
|
||||
"user_agent": request.headers.get("User-Agent", ""),
|
||||
"referrer": request.headers.get("Referer", ""),
|
||||
"comment_type": "comment",
|
||||
"comment_author": "",
|
||||
}
|
||||
|
||||
return ak.comment_check(smart_str(text), data=ak_data)
|
||||
@ -131,12 +150,12 @@ def text_is_spam(text, request):
|
||||
|
||||
def process_attachments(followup, attached_files):
|
||||
max_email_attachment_size = getattr(
|
||||
settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000)
|
||||
settings, "HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE", 512000
|
||||
)
|
||||
attachments = []
|
||||
errors = set()
|
||||
|
||||
for attached in attached_files:
|
||||
|
||||
if attached.size:
|
||||
from helpdesk.models import FollowUpAttachment
|
||||
|
||||
@ -145,9 +164,9 @@ def process_attachments(followup, attached_files):
|
||||
followup=followup,
|
||||
file=attached,
|
||||
filename=filename,
|
||||
mime_type=attached.content_type or
|
||||
mimetypes.guess_type(filename, strict=False)[0] or
|
||||
'application/octet-stream',
|
||||
mime_type=attached.content_type
|
||||
or mimetypes.guess_type(filename, strict=False)[0]
|
||||
or "application/octet-stream",
|
||||
size=attached.size,
|
||||
)
|
||||
try:
|
||||
@ -176,7 +195,7 @@ def format_time_spent(time_spent):
|
||||
if time_spent:
|
||||
time_spent = "{0:02d}h:{1:02d}m".format(
|
||||
int(time_spent.total_seconds()) // 3600,
|
||||
int(time_spent.total_seconds()) % 3600 // 60
|
||||
int(time_spent.total_seconds()) % 3600 // 60,
|
||||
)
|
||||
else:
|
||||
time_spent = ""
|
||||
@ -184,7 +203,7 @@ def format_time_spent(time_spent):
|
||||
|
||||
|
||||
def convert_value(value):
|
||||
""" Convert date/time data type to known fixed format string """
|
||||
"""Convert date/time data type to known fixed format string"""
|
||||
if type(value) is datetime:
|
||||
return value.strftime(CUSTOMFIELD_DATETIME_FORMAT)
|
||||
elif type(value) is date:
|
||||
@ -201,39 +220,65 @@ def daily_time_spent_calculation(earliest, latest, open_hours):
|
||||
time_spent_seconds = 0
|
||||
|
||||
# avoid rendering day in different locale
|
||||
weekday = ('monday', 'tuesday', 'wednesday', 'thursday',
|
||||
'friday', 'saturday', 'sunday')[earliest.weekday()]
|
||||
|
||||
weekday = (
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday",
|
||||
)[earliest.weekday()]
|
||||
|
||||
# enforce correct settings
|
||||
MIDNIGHT = 23.9999
|
||||
start, end = open_hours.get(weekday, (0, MIDNIGHT))
|
||||
if not 0 <= start <= end <= MIDNIGHT:
|
||||
raise ImproperlyConfigured("HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS"
|
||||
f" setting for {weekday} out of (0, 23.9999) boundary")
|
||||
|
||||
raise ImproperlyConfigured(
|
||||
"HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS"
|
||||
f" setting for {weekday} out of (0, 23.9999) boundary"
|
||||
)
|
||||
|
||||
# transform decimals to minutes and seconds
|
||||
start_hour, start_minute, start_second = int(start), int(start % 1 * 60), int(start * 60 % 1 * 60)
|
||||
end_hour, end_minute, end_second = int(end), int(end % 1 * 60), int(end * 60 % 1 * 60)
|
||||
start_hour, start_minute, start_second = (
|
||||
int(start),
|
||||
int(start % 1 * 60),
|
||||
int(start * 60 % 1 * 60),
|
||||
)
|
||||
end_hour, end_minute, end_second = (
|
||||
int(end),
|
||||
int(end % 1 * 60),
|
||||
int(end * 60 % 1 * 60),
|
||||
)
|
||||
|
||||
# translate time for delta calculation
|
||||
earliest_f = earliest.hour + earliest.minute / 60 + earliest.second / 3600
|
||||
latest_f = latest.hour + latest.minute / 60 + latest.second / (60 * 60) + latest.microsecond / (60 * 60 * 999999)
|
||||
latest_f = (
|
||||
latest.hour
|
||||
+ latest.minute / 60
|
||||
+ latest.second / (60 * 60)
|
||||
+ latest.microsecond / (60 * 60 * 999999)
|
||||
)
|
||||
|
||||
# if latest time is midnight and close hour is midnight, add a second to the time spent
|
||||
if latest_f >= MIDNIGHT and end == MIDNIGHT:
|
||||
time_spent_seconds += 1
|
||||
|
||||
|
||||
if earliest_f < start:
|
||||
earliest = earliest.replace(hour=start_hour, minute=start_minute, second=start_second)
|
||||
earliest = earliest.replace(
|
||||
hour=start_hour, minute=start_minute, second=start_second
|
||||
)
|
||||
elif earliest_f >= end:
|
||||
earliest = earliest.replace(hour=end_hour, minute=end_minute, second=end_second)
|
||||
|
||||
|
||||
if latest_f < start:
|
||||
latest = latest.replace(hour=start_hour, minute=start_minute, second=start_second)
|
||||
latest = latest.replace(
|
||||
hour=start_hour, minute=start_minute, second=start_second
|
||||
)
|
||||
elif latest_f >= end:
|
||||
latest = latest.replace(hour=end_hour, minute=end_minute, second=end_second)
|
||||
|
||||
|
||||
day_delta = latest - earliest
|
||||
time_spent_seconds += day_delta.seconds
|
||||
|
||||
return time_spent_seconds
|
||||
|
||||
return time_spent_seconds
|
||||
|
@ -14,56 +14,56 @@ from django.core.management.base import BaseCommand, CommandError
|
||||
from helpdesk.models import EscalationExclusion, Queue
|
||||
|
||||
day_names = {
|
||||
'monday': 0,
|
||||
'tuesday': 1,
|
||||
'wednesday': 2,
|
||||
'thursday': 3,
|
||||
'friday': 4,
|
||||
'saturday': 5,
|
||||
'sunday': 6,
|
||||
"monday": 0,
|
||||
"tuesday": 1,
|
||||
"wednesday": 2,
|
||||
"thursday": 3,
|
||||
"friday": 4,
|
||||
"saturday": 5,
|
||||
"sunday": 6,
|
||||
}
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'-d',
|
||||
'--days',
|
||||
nargs='*',
|
||||
"-d",
|
||||
"--days",
|
||||
nargs="*",
|
||||
choices=list(day_names.keys()),
|
||||
required=True,
|
||||
help='Days of week (monday, tuesday, etc). Enter the days as space separated list.'
|
||||
help="Days of week (monday, tuesday, etc). Enter the days as space separated list.",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-o',
|
||||
'--occurrences',
|
||||
"-o",
|
||||
"--occurrences",
|
||||
default=1,
|
||||
type=int,
|
||||
help='Occurrences: How many weeks ahead to exclude this day'
|
||||
help="Occurrences: How many weeks ahead to exclude this day",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-q',
|
||||
'--queues',
|
||||
nargs='*',
|
||||
choices=list(Queue.objects.values_list('slug', flat=True)),
|
||||
help='Queues to include (default: all). Enter the queues slug as space separated list.'
|
||||
"-q",
|
||||
"--queues",
|
||||
nargs="*",
|
||||
choices=list(Queue.objects.values_list("slug", flat=True)),
|
||||
help="Queues to include (default: all). Enter the queues slug as space separated list.",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-x',
|
||||
'--exclude-verbosely',
|
||||
action='store_true',
|
||||
"-x",
|
||||
"--exclude-verbosely",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help='Display a list of dates excluded'
|
||||
help="Display a list of dates excluded",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
days = options['days']
|
||||
occurrences = options['occurrences']
|
||||
verbose = options['exclude_verbosely']
|
||||
queue_slugs = options['queues']
|
||||
days = options["days"]
|
||||
occurrences = options["occurrences"]
|
||||
verbose = options["exclude_verbosely"]
|
||||
queue_slugs = options["queues"]
|
||||
|
||||
if not (days and occurrences):
|
||||
raise CommandError('One or more occurrences must be specified.')
|
||||
raise CommandError("One or more occurrences must be specified.")
|
||||
|
||||
queues = []
|
||||
if queue_slugs is not None:
|
||||
@ -77,12 +77,13 @@ class Command(BaseCommand):
|
||||
if day == workdate.weekday():
|
||||
if EscalationExclusion.objects.filter(date=workdate).count() == 0:
|
||||
esc = EscalationExclusion.objects.create(
|
||||
name=f'Auto Exclusion for {day_name}',
|
||||
date=workdate
|
||||
name=f"Auto Exclusion for {day_name}", date=workdate
|
||||
)
|
||||
|
||||
if verbose:
|
||||
self.stdout.write(f"Created exclusion for {day_name} {workdate}")
|
||||
self.stdout.write(
|
||||
f"Created exclusion for {day_name} {workdate}"
|
||||
)
|
||||
|
||||
for q in queues:
|
||||
esc.queues.add(q)
|
||||
|
@ -22,25 +22,24 @@ from helpdesk.models import Queue
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'-q',
|
||||
'--queues',
|
||||
nargs='*',
|
||||
choices=list(Queue.objects.values_list('slug', flat=True)),
|
||||
help='Queues to include (default: all). Enter the queues slug as space separated list.'
|
||||
"-q",
|
||||
"--queues",
|
||||
nargs="*",
|
||||
choices=list(Queue.objects.values_list("slug", flat=True)),
|
||||
help="Queues to include (default: all). Enter the queues slug as space separated list.",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-x',
|
||||
'--escalate-verbosely',
|
||||
action='store_true',
|
||||
"-x",
|
||||
"--escalate-verbosely",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help='Display a list of dates excluded'
|
||||
help="Display a list of dates excluded",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
queue_slugs = options['queues']
|
||||
queue_slugs = options["queues"]
|
||||
|
||||
if queue_slugs is not None:
|
||||
queues = Queue.objects.filter(slug__in=queue_slugs)
|
||||
@ -53,16 +52,17 @@ class Command(BaseCommand):
|
||||
|
||||
if q.permission_name:
|
||||
self.stdout.write(
|
||||
f" .. already has `permission_name={q.permission_name}`")
|
||||
f" .. already has `permission_name={q.permission_name}`"
|
||||
)
|
||||
basename = q.permission_name[9:]
|
||||
else:
|
||||
basename = q.generate_permission_name()
|
||||
self.stdout.write(
|
||||
f" .. generated `permission_name={q.permission_name}`")
|
||||
f" .. generated `permission_name={q.permission_name}`"
|
||||
)
|
||||
q.save()
|
||||
|
||||
self.stdout.write(
|
||||
f" .. checking permission codename `{basename}`")
|
||||
self.stdout.write(f" .. checking permission codename `{basename}`")
|
||||
|
||||
try:
|
||||
Permission.objects.create(
|
||||
|
@ -20,10 +20,12 @@ User = get_user_model()
|
||||
class Command(BaseCommand):
|
||||
"""create_usersettings command"""
|
||||
|
||||
help = _('Check for user without django-helpdesk UserSettings '
|
||||
'and create settings if required. Uses '
|
||||
'settings.DEFAULT_USER_SETTINGS which can be overridden to '
|
||||
'suit your situation.')
|
||||
help = _(
|
||||
"Check for user without django-helpdesk UserSettings "
|
||||
"and create settings if required. Uses "
|
||||
"settings.DEFAULT_USER_SETTINGS which can be overridden to "
|
||||
"suit your situation."
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""handle command line"""
|
||||
|
@ -20,34 +20,36 @@ from helpdesk.models import EscalationExclusion, Queue, Ticket
|
||||
class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'-q',
|
||||
'--queues',
|
||||
nargs='*',
|
||||
choices=list(Queue.objects.values_list('slug', flat=True)),
|
||||
help='Queues to include (default: all). Enter the queues slug as space separated list.'
|
||||
"-q",
|
||||
"--queues",
|
||||
nargs="*",
|
||||
choices=list(Queue.objects.values_list("slug", flat=True)),
|
||||
help="Queues to include (default: all). Enter the queues slug as space separated list.",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-x',
|
||||
'--escalate-verbosely',
|
||||
action='store_true',
|
||||
"-x",
|
||||
"--escalate-verbosely",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help='Display escalated tickets'
|
||||
help="Display escalated tickets",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-n',
|
||||
'--notify-only',
|
||||
action='store_true',
|
||||
"-n",
|
||||
"--notify-only",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help='Send email reminder but dont escalate tickets'
|
||||
help="Send email reminder but dont escalate tickets",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
verbose = options['escalate_verbosely']
|
||||
notify_only = options['notify_only']
|
||||
verbose = options["escalate_verbosely"]
|
||||
notify_only = options["notify_only"]
|
||||
|
||||
queue_slugs = options['queues']
|
||||
queue_slugs = options["queues"]
|
||||
# Only include queues with escalation configured
|
||||
queues = Queue.objects.filter(escalate_days__isnull=False).exclude(escalate_days=0)
|
||||
queues = Queue.objects.filter(escalate_days__isnull=False).exclude(
|
||||
escalate_days=0
|
||||
)
|
||||
if queue_slugs is not None:
|
||||
queues = queues.filter(slug__in=queue_slugs)
|
||||
|
||||
@ -68,17 +70,15 @@ class Command(BaseCommand):
|
||||
|
||||
req_last_escl_date = timezone.now() - timedelta(days=days)
|
||||
|
||||
for ticket in queue.ticket_set.filter(
|
||||
status__in=Ticket.OPEN_STATUSES
|
||||
).exclude(
|
||||
priority=1
|
||||
).filter(
|
||||
Q(on_hold__isnull=True) | Q(on_hold=False)
|
||||
).filter(
|
||||
Q(last_escalation__lte=req_last_escl_date) |
|
||||
Q(last_escalation__isnull=True, created__lte=req_last_escl_date)
|
||||
for ticket in (
|
||||
queue.ticket_set.filter(status__in=Ticket.OPEN_STATUSES)
|
||||
.exclude(priority=1)
|
||||
.filter(Q(on_hold__isnull=True) | Q(on_hold=False))
|
||||
.filter(
|
||||
Q(last_escalation__lte=req_last_escl_date)
|
||||
| Q(last_escalation__isnull=True, created__lte=req_last_escl_date)
|
||||
)
|
||||
):
|
||||
|
||||
ticket.last_escalation = timezone.now()
|
||||
ticket.priority -= 1
|
||||
ticket.save()
|
||||
@ -86,24 +86,29 @@ class Command(BaseCommand):
|
||||
context = safe_template_context(ticket)
|
||||
|
||||
ticket.send(
|
||||
{'submitter': ('escalated_submitter', context),
|
||||
'ticket_cc': ('escalated_cc', context),
|
||||
'assigned_to': ('escalated_owner', context)},
|
||||
{
|
||||
"submitter": ("escalated_submitter", context),
|
||||
"ticket_cc": ("escalated_cc", context),
|
||||
"assigned_to": ("escalated_owner", context),
|
||||
},
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if verbose:
|
||||
self.stdout.write(f" - Esclating {ticket.ticket} from {ticket.priority + 1}>{ticket.priority}")
|
||||
self.stdout.write(
|
||||
f" - Esclating {ticket.ticket} from {ticket.priority + 1}>{ticket.priority}"
|
||||
)
|
||||
|
||||
if not notify_only:
|
||||
followup = ticket.followup_set.create(
|
||||
title=_('Ticket Escalated'),
|
||||
title=_("Ticket Escalated"),
|
||||
public=True,
|
||||
comment=_('Ticket escalated after %(nb)s days') % {'nb': queue.escalate_days},
|
||||
comment=_("Ticket escalated after %(nb)s days")
|
||||
% {"nb": queue.escalate_days},
|
||||
)
|
||||
|
||||
followup.ticketchange_set.create(
|
||||
field=_('Priority'),
|
||||
field=_("Priority"),
|
||||
old_value=ticket.priority + 1,
|
||||
new_value=ticket.priority,
|
||||
)
|
||||
|
@ -10,36 +10,38 @@ scripts/get_email.py - Designed to be run from cron, this script checks the
|
||||
helpdesk, creating tickets from the new messages (or
|
||||
adding to existing tickets if needed)
|
||||
"""
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from helpdesk.email import process_email
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
help = 'Process django-helpdesk queues and process e-mails via POP3/IMAP or ' \
|
||||
'from a local mailbox directory as required, feeding them into the helpdesk.'
|
||||
help = (
|
||||
"Process django-helpdesk queues and process e-mails via POP3/IMAP or "
|
||||
"from a local mailbox directory as required, feeding them into the helpdesk."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--quiet',
|
||||
action='store_true',
|
||||
dest='quiet',
|
||||
"--quiet",
|
||||
action="store_true",
|
||||
dest="quiet",
|
||||
default=False,
|
||||
help='Hide details about each queue/message as they are processed',
|
||||
help="Hide details about each queue/message as they are processed",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--debug_to_stdout',
|
||||
action='store_true',
|
||||
dest='debug_to_stdout',
|
||||
"--debug_to_stdout",
|
||||
action="store_true",
|
||||
dest="debug_to_stdout",
|
||||
default=False,
|
||||
help='Log additional messaging to stdout.',
|
||||
help="Log additional messaging to stdout.",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
quiet = options.get('quiet')
|
||||
debug_to_stdout = options.get('debug_to_stdout')
|
||||
quiet = options.get("quiet")
|
||||
debug_to_stdout = options.get("debug_to_stdout")
|
||||
process_email(quiet=quiet, debug_to_stdout=debug_to_stdout)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
process_email()
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@ def pickle_settings(data):
|
||||
except ImportError:
|
||||
import cPickle as pickle
|
||||
from helpdesk.query import b64encode
|
||||
|
||||
return b64encode(pickle.dumps(data))
|
||||
|
||||
|
||||
@ -38,14 +39,12 @@ def populate_usersettings(apps, schema_editor):
|
||||
|
||||
noop = lambda *args, **kwargs: None
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('helpdesk', '0001_initial'),
|
||||
("helpdesk", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(populate_usersettings, reverse_code=noop),
|
||||
]
|
||||
|
||||
|
||||
|
@ -4,14 +4,15 @@ import os
|
||||
from django.db import migrations
|
||||
from django.core import serializers
|
||||
|
||||
fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures'))
|
||||
fixture_filename = 'emailtemplate.json'
|
||||
fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../fixtures"))
|
||||
fixture_filename = "emailtemplate.json"
|
||||
|
||||
|
||||
def deserialize_fixture():
|
||||
fixture_file = os.path.join(fixture_dir, fixture_filename)
|
||||
|
||||
with open(fixture_file, 'rb') as fixture:
|
||||
return list(serializers.deserialize('json', fixture, ignorenonexistent=True))
|
||||
with open(fixture_file, "rb") as fixture:
|
||||
return list(serializers.deserialize("json", fixture, ignorenonexistent=True))
|
||||
|
||||
|
||||
def load_fixture(apps, schema_editor):
|
||||
@ -27,13 +28,12 @@ def unload_fixture(apps, schema_editor):
|
||||
objects = deserialize_fixture()
|
||||
|
||||
EmailTemplate = apps.get_model("helpdesk", "emailtemplate")
|
||||
EmailTemplate.objects.filter(pk__in=[ obj.object.pk for obj in objects ]).delete()
|
||||
EmailTemplate.objects.filter(pk__in=[obj.object.pk for obj in objects]).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0002_populate_usersettings'),
|
||||
("helpdesk", "0002_populate_usersettings"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
@ -4,23 +4,42 @@ from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('helpdesk', '0003_initial_data_import'),
|
||||
("helpdesk", "0003_initial_data_import"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='QueueMembership',
|
||||
name="QueueMembership",
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('queues', models.ManyToManyField(to='helpdesk.Queue', verbose_name='Authorized Queues')),
|
||||
('user', models.OneToOneField(verbose_name='User', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
verbose_name="ID",
|
||||
serialize=False,
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"queues",
|
||||
models.ManyToManyField(
|
||||
to="helpdesk.Queue", verbose_name="Authorized Queues"
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.OneToOneField(
|
||||
verbose_name="User",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Queue Membership',
|
||||
'verbose_name_plural': 'Queue Memberships',
|
||||
"verbose_name": "Queue Membership",
|
||||
"verbose_name_plural": "Queue Memberships",
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
|
@ -3,25 +3,36 @@ from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0004_add_per_queue_staff_membership'),
|
||||
("helpdesk", "0004_add_per_queue_staff_membership"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='escalationexclusion',
|
||||
name='queues',
|
||||
field=models.ManyToManyField(help_text='Leave blank for this exclusion to be applied to all queues, or select those queues you wish to exclude with this entry.', to='helpdesk.Queue', blank=True),
|
||||
model_name="escalationexclusion",
|
||||
name="queues",
|
||||
field=models.ManyToManyField(
|
||||
help_text="Leave blank for this exclusion to be applied to all queues, or select those queues you wish to exclude with this entry.",
|
||||
to="helpdesk.Queue",
|
||||
blank=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ignoreemail',
|
||||
name='queues',
|
||||
field=models.ManyToManyField(help_text='Leave blank for this e-mail to be ignored on all queues, or select those queues you wish to ignore this e-mail for.', to='helpdesk.Queue', blank=True),
|
||||
model_name="ignoreemail",
|
||||
name="queues",
|
||||
field=models.ManyToManyField(
|
||||
help_text="Leave blank for this e-mail to be ignored on all queues, or select those queues you wish to ignore this e-mail for.",
|
||||
to="helpdesk.Queue",
|
||||
blank=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='presetreply',
|
||||
name='queues',
|
||||
field=models.ManyToManyField(help_text='Leave blank to allow this reply to be used for all queues, or select those queues you wish to limit this reply to.', to='helpdesk.Queue', blank=True),
|
||||
model_name="presetreply",
|
||||
name="queues",
|
||||
field=models.ManyToManyField(
|
||||
help_text="Leave blank to allow this reply to be used for all queues, or select those queues you wish to limit this reply to.",
|
||||
to="helpdesk.Queue",
|
||||
blank=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -3,25 +3,42 @@ from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0005_queues_no_null'),
|
||||
("helpdesk", "0005_queues_no_null"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='email_address',
|
||||
field=models.EmailField(help_text='All outgoing e-mails for this queue will use this e-mail address. If you use IMAP or POP3, this should be the e-mail address for that mailbox.', max_length=254, null=True, verbose_name='E-Mail Address', blank=True),
|
||||
model_name="queue",
|
||||
name="email_address",
|
||||
field=models.EmailField(
|
||||
help_text="All outgoing e-mails for this queue will use this e-mail address. If you use IMAP or POP3, this should be the e-mail address for that mailbox.",
|
||||
max_length=254,
|
||||
null=True,
|
||||
verbose_name="E-Mail Address",
|
||||
blank=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticket',
|
||||
name='submitter_email',
|
||||
field=models.EmailField(help_text='The submitter will receive an email for all public follow-ups left for this task.', max_length=254, null=True, verbose_name='Submitter E-Mail', blank=True),
|
||||
model_name="ticket",
|
||||
name="submitter_email",
|
||||
field=models.EmailField(
|
||||
help_text="The submitter will receive an email for all public follow-ups left for this task.",
|
||||
max_length=254,
|
||||
null=True,
|
||||
verbose_name="Submitter E-Mail",
|
||||
blank=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticketcc',
|
||||
name='email',
|
||||
field=models.EmailField(help_text='For non-user followers, enter their e-mail address', max_length=254, null=True, verbose_name='E-Mail Address', blank=True),
|
||||
model_name="ticketcc",
|
||||
name="email",
|
||||
field=models.EmailField(
|
||||
help_text="For non-user followers, enter their e-mail address",
|
||||
max_length=254,
|
||||
null=True,
|
||||
verbose_name="E-Mail Address",
|
||||
blank=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -3,15 +3,18 @@ from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0006_email_maxlength'),
|
||||
("helpdesk", "0006_email_maxlength"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='label',
|
||||
field=models.CharField(help_text='The display label for this field', max_length=30, verbose_name='Label'),
|
||||
model_name="customfield",
|
||||
name="label",
|
||||
field=models.CharField(
|
||||
help_text="The display label for this field",
|
||||
max_length=30,
|
||||
verbose_name="Label",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -3,15 +3,20 @@ from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0007_max_length_by_integer'),
|
||||
("helpdesk", "0007_max_length_by_integer"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='queue',
|
||||
name='permission_name',
|
||||
field=models.CharField(help_text='Name used in the django.contrib.auth permission system', max_length=50, null=True, verbose_name='Django auth permission name', blank=True),
|
||||
model_name="queue",
|
||||
name="permission_name",
|
||||
field=models.CharField(
|
||||
help_text="Name used in the django.contrib.auth permission system",
|
||||
max_length=50,
|
||||
null=True,
|
||||
verbose_name="Django auth permission name",
|
||||
blank=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -7,14 +7,14 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
def create_and_assign_permissions(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
Permission = apps.get_model('auth', 'Permission')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Permission = apps.get_model("auth", "Permission")
|
||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||
# Two steps:
|
||||
# 1. Create the permission for existing Queues
|
||||
# 2. Assign the permission to user according to QueueMembership objects
|
||||
|
||||
# First step: prepare the permission for each queue
|
||||
Queue = apps.get_model('helpdesk', 'Queue')
|
||||
Queue = apps.get_model("helpdesk", "Queue")
|
||||
for q in Queue.objects.using(db_alias).all():
|
||||
if not q.permission_name:
|
||||
basename = "queue_access_%s" % q.slug
|
||||
@ -35,7 +35,7 @@ def create_and_assign_permissions(apps, schema_editor):
|
||||
q.save()
|
||||
|
||||
# Second step: map the permissions according to QueueMembership
|
||||
QueueMembership = apps.get_model('helpdesk', 'QueueMembership')
|
||||
QueueMembership = apps.get_model("helpdesk", "QueueMembership")
|
||||
for qm in QueueMembership.objects.using(db_alias).all():
|
||||
user = qm.user
|
||||
for q in qm.queues.all():
|
||||
@ -47,9 +47,9 @@ def create_and_assign_permissions(apps, schema_editor):
|
||||
|
||||
def revert_queue_membership(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
Permission = apps.get_model('auth', 'Permission')
|
||||
Queue = apps.get_model('helpdesk', 'Queue')
|
||||
QueueMembership = apps.get_model('helpdesk', 'QueueMembership')
|
||||
Permission = apps.get_model("auth", "Permission")
|
||||
Queue = apps.get_model("helpdesk", "Queue")
|
||||
QueueMembership = apps.get_model("helpdesk", "QueueMembership")
|
||||
for p in Permission.objects.using(db_alias).all():
|
||||
if p.codename.startswith("queue_access_"):
|
||||
slug = p.codename[13:]
|
||||
@ -66,12 +66,10 @@ def revert_queue_membership(apps, schema_editor):
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0008_extra_for_permissions'),
|
||||
("helpdesk", "0008_extra_for_permissions"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_and_assign_permissions,
|
||||
revert_queue_membership)
|
||||
migrations.RunPython(create_and_assign_permissions, revert_queue_membership)
|
||||
]
|
||||
|
@ -3,21 +3,20 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0009_migrate_queuemembership'),
|
||||
("helpdesk", "0009_migrate_queuemembership"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='queuemembership',
|
||||
name='queues',
|
||||
model_name="queuemembership",
|
||||
name="queues",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='queuemembership',
|
||||
name='user',
|
||||
model_name="queuemembership",
|
||||
name="user",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='QueueMembership',
|
||||
name="QueueMembership",
|
||||
),
|
||||
]
|
||||
|
@ -3,20 +3,30 @@ from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0010_remove_queuemembership'),
|
||||
("helpdesk", "0010_remove_queuemembership"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='permission_name',
|
||||
field=models.CharField(editable=False, max_length=50, blank=True, help_text='Name used in the django.contrib.auth permission system', null=True, verbose_name='Django auth permission name'),
|
||||
model_name="queue",
|
||||
name="permission_name",
|
||||
field=models.CharField(
|
||||
editable=False,
|
||||
max_length=50,
|
||||
blank=True,
|
||||
help_text="Name used in the django.contrib.auth permission system",
|
||||
null=True,
|
||||
verbose_name="Django auth permission name",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text="This slug is used when building ticket ID's. Once set, try not to change it or e-mailing may get messy.", unique=True, verbose_name='Slug'),
|
||||
model_name="queue",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
help_text="This slug is used when building ticket ID's. Once set, try not to change it or e-mailing may get messy.",
|
||||
unique=True,
|
||||
verbose_name="Slug",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -6,16 +6,22 @@ import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('helpdesk', '0011_admin_related_improvements'),
|
||||
("helpdesk", "0011_admin_related_improvements"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='queue',
|
||||
name='default_owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='default_owner', to=settings.AUTH_USER_MODEL, verbose_name='Default owner'),
|
||||
model_name="queue",
|
||||
name="default_owner",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="default_owner",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Default owner",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -4,30 +4,66 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0012_queue_default_owner'),
|
||||
("helpdesk", "0012_queue_default_owner"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='queue',
|
||||
name='email_box_local_dir',
|
||||
field=models.CharField(blank=True, help_text='If using a local directory, what directory path do you wish to poll for new email? Example: /var/lib/mail/helpdesk/', max_length=200, null=True, verbose_name='E-Mail Local Directory'),
|
||||
model_name="queue",
|
||||
name="email_box_local_dir",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="If using a local directory, what directory path do you wish to poll for new email? Example: /var/lib/mail/helpdesk/",
|
||||
max_length=200,
|
||||
null=True,
|
||||
verbose_name="E-Mail Local Directory",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='queue',
|
||||
name='logging_dir',
|
||||
field=models.CharField(blank=True, help_text='If logging is enabled, what directory should we use to store log files for this queue? The standard logging mechanims are used if no directory is set', max_length=200, null=True, verbose_name='Logging Directory'),
|
||||
model_name="queue",
|
||||
name="logging_dir",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="If logging is enabled, what directory should we use to store log files for this queue? The standard logging mechanims are used if no directory is set",
|
||||
max_length=200,
|
||||
null=True,
|
||||
verbose_name="Logging Directory",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='queue',
|
||||
name='logging_type',
|
||||
field=models.CharField(blank=True, choices=[('none', 'None'), ('debug', 'Debug'), ('info', 'Information'), ('warn', 'Warning'), ('error', 'Error'), ('crit', 'Critical')], help_text='Set the default logging level. All messages at that level or above will be logged to the directory set below. If no level is set, logging will be disabled.', max_length=5, null=True, verbose_name='Logging Type'),
|
||||
model_name="queue",
|
||||
name="logging_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("none", "None"),
|
||||
("debug", "Debug"),
|
||||
("info", "Information"),
|
||||
("warn", "Warning"),
|
||||
("error", "Error"),
|
||||
("crit", "Critical"),
|
||||
],
|
||||
help_text="Set the default logging level. All messages at that level or above will be logged to the directory set below. If no level is set, logging will be disabled.",
|
||||
max_length=5,
|
||||
null=True,
|
||||
verbose_name="Logging Type",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='email_box_type',
|
||||
field=models.CharField(blank=True, choices=[('pop3', 'POP 3'), ('imap', 'IMAP'), ('local', 'Local Directory')], help_text='E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.', max_length=5, null=True, verbose_name='E-Mail Box Type'),
|
||||
model_name="queue",
|
||||
name="email_box_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("pop3", "POP 3"),
|
||||
("imap", "IMAP"),
|
||||
("local", "Local Directory"),
|
||||
],
|
||||
help_text="E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.",
|
||||
max_length=5,
|
||||
null=True,
|
||||
verbose_name="E-Mail Box Type",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -4,17 +4,18 @@ from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0013_email_box_local_dir_and_logging'),
|
||||
("helpdesk", "0013_email_box_local_dir_and_logging"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='usersettings',
|
||||
name='user',
|
||||
field=models.OneToOneField(to=settings.AUTH_USER_MODEL,
|
||||
related_name='usersettings_helpdesk',
|
||||
on_delete=models.CASCADE),
|
||||
model_name="usersettings",
|
||||
name="user",
|
||||
field=models.OneToOneField(
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
related_name="usersettings_helpdesk",
|
||||
on_delete=models.CASCADE,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -4,15 +4,21 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0014_usersettings_related_name'),
|
||||
("helpdesk", "0014_usersettings_related_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='permission_name',
|
||||
field=models.CharField(blank=True, editable=False, help_text='Name used in the django.contrib.auth permission system', max_length=72, null=True, verbose_name='Django auth permission name'),
|
||||
model_name="queue",
|
||||
name="permission_name",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
editable=False,
|
||||
help_text="Name used in the django.contrib.auth permission system",
|
||||
max_length=72,
|
||||
null=True,
|
||||
verbose_name="Django auth permission name",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -4,38 +4,61 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0015_expand_permission_name_size'),
|
||||
("helpdesk", "0015_expand_permission_name_size"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='attachment',
|
||||
options={'ordering': ('filename',), 'verbose_name': 'Attachment', 'verbose_name_plural': 'Attachments'},
|
||||
name="attachment",
|
||||
options={
|
||||
"ordering": ("filename",),
|
||||
"verbose_name": "Attachment",
|
||||
"verbose_name_plural": "Attachments",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='emailtemplate',
|
||||
options={'ordering': ('template_name', 'locale'), 'verbose_name': 'e-mail template', 'verbose_name_plural': 'e-mail templates'},
|
||||
name="emailtemplate",
|
||||
options={
|
||||
"ordering": ("template_name", "locale"),
|
||||
"verbose_name": "e-mail template",
|
||||
"verbose_name_plural": "e-mail templates",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='followup',
|
||||
options={'ordering': ('date',), 'verbose_name': 'Follow-up', 'verbose_name_plural': 'Follow-ups'},
|
||||
name="followup",
|
||||
options={
|
||||
"ordering": ("date",),
|
||||
"verbose_name": "Follow-up",
|
||||
"verbose_name_plural": "Follow-ups",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='kbcategory',
|
||||
options={'ordering': ('title',), 'verbose_name': 'Knowledge base category', 'verbose_name_plural': 'Knowledge base categories'},
|
||||
name="kbcategory",
|
||||
options={
|
||||
"ordering": ("title",),
|
||||
"verbose_name": "Knowledge base category",
|
||||
"verbose_name_plural": "Knowledge base categories",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='kbitem',
|
||||
options={'ordering': ('title',), 'verbose_name': 'Knowledge base item', 'verbose_name_plural': 'Knowledge base items'},
|
||||
name="kbitem",
|
||||
options={
|
||||
"ordering": ("title",),
|
||||
"verbose_name": "Knowledge base item",
|
||||
"verbose_name_plural": "Knowledge base items",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='presetreply',
|
||||
options={'ordering': ('name',), 'verbose_name': 'Pre-set reply', 'verbose_name_plural': 'Pre-set replies'},
|
||||
name="presetreply",
|
||||
options={
|
||||
"ordering": ("name",),
|
||||
"verbose_name": "Pre-set reply",
|
||||
"verbose_name_plural": "Pre-set replies",
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='ticketcustomfieldvalue',
|
||||
unique_together=set([('ticket', 'field')]),
|
||||
name="ticketcustomfieldvalue",
|
||||
unique_together=set([("ticket", "field")]),
|
||||
),
|
||||
]
|
||||
|
@ -6,15 +6,21 @@ import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0016_alter_model_options'),
|
||||
("helpdesk", "0016_alter_model_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='default_owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_owner', to=settings.AUTH_USER_MODEL, verbose_name='Default owner'),
|
||||
model_name="queue",
|
||||
name="default_owner",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="default_owner",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Default owner",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -4,55 +4,99 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0017_default_owner_on_delete_null'),
|
||||
("helpdesk", "0017_default_owner_on_delete_null"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='followup',
|
||||
name='public',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.', verbose_name='Public'),
|
||||
model_name="followup",
|
||||
name="public",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.",
|
||||
verbose_name="Public",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ignoreemail',
|
||||
name='keep_in_mailbox',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.', verbose_name='Save Emails in Mailbox?'),
|
||||
model_name="ignoreemail",
|
||||
name="keep_in_mailbox",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.",
|
||||
verbose_name="Save Emails in Mailbox?",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='allow_email_submission',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Do you want to poll the e-mail box below for new tickets?', verbose_name='Allow E-Mail Submission?'),
|
||||
model_name="queue",
|
||||
name="allow_email_submission",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="Do you want to poll the e-mail box below for new tickets?",
|
||||
verbose_name="Allow E-Mail Submission?",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='allow_public_submission',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Should this queue be listed on the public submission form?', verbose_name='Allow Public Submission?'),
|
||||
model_name="queue",
|
||||
name="allow_public_submission",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="Should this queue be listed on the public submission form?",
|
||||
verbose_name="Allow Public Submission?",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='email_box_ssl',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.', verbose_name='Use SSL for E-Mail?'),
|
||||
model_name="queue",
|
||||
name="email_box_ssl",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.",
|
||||
verbose_name="Use SSL for E-Mail?",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='savedsearch',
|
||||
name='shared',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Should other users see this query?', verbose_name='Shared With Other Users?'),
|
||||
model_name="savedsearch",
|
||||
name="shared",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="Should other users see this query?",
|
||||
verbose_name="Shared With Other Users?",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticket',
|
||||
name='on_hold',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='If a ticket is on hold, it will not automatically be escalated.', verbose_name='On Hold'),
|
||||
model_name="ticket",
|
||||
name="on_hold",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="If a ticket is on hold, it will not automatically be escalated.",
|
||||
verbose_name="On Hold",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticketcc',
|
||||
name='can_update',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Can this CC login and update the ticket?', verbose_name='Can Update Ticket?'),
|
||||
model_name="ticketcc",
|
||||
name="can_update",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="Can this CC login and update the ticket?",
|
||||
verbose_name="Can Update Ticket?",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticketcc',
|
||||
name='can_view',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Can this CC login to view the ticket details?', verbose_name='Can View Ticket?'),
|
||||
model_name="ticketcc",
|
||||
name="can_view",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="Can this CC login to view the ticket details?",
|
||||
verbose_name="Can View Ticket?",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -8,22 +8,24 @@ def clear_secret_keys(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
for ticket in Ticket.objects.using(db_alias).all():
|
||||
ticket.secret_key = ''
|
||||
ticket.secret_key = ""
|
||||
ticket.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0018_ticket_secret_key'),
|
||||
("helpdesk", "0018_ticket_secret_key"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='secret_key',
|
||||
field=models.CharField(default=helpdesk.models.mk_secret, max_length=36,
|
||||
verbose_name='Secret key needed for viewing/editing ticket by non-logged in users'),
|
||||
model_name="ticket",
|
||||
name="secret_key",
|
||||
field=models.CharField(
|
||||
default=helpdesk.models.mk_secret,
|
||||
max_length=36,
|
||||
verbose_name="Secret key needed for viewing/editing ticket by non-logged in users",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(clear_secret_keys),
|
||||
]
|
||||
|
@ -16,7 +16,7 @@ def unpickle_settings(settings_pickled):
|
||||
# Python 3 support
|
||||
from base64 import decodebytes as b64decode
|
||||
try:
|
||||
return pickle.loads(b64decode(settings_pickled.encode('utf-8')))
|
||||
return pickle.loads(b64decode(settings_pickled.encode("utf-8")))
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
@ -33,41 +33,66 @@ def move_old_values(apps, schema_editor):
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0019_ticket_secret_key'),
|
||||
("helpdesk", "0019_ticket_secret_key"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='usersettings',
|
||||
name='email_on_ticket_assign',
|
||||
field=models.BooleanField(default=helpdesk.models.email_on_ticket_assign_default, help_text='If you are assigned a ticket via the web, do you want to receive an e-mail?', verbose_name='E-mail me when assigned a ticket?'),
|
||||
model_name="usersettings",
|
||||
name="email_on_ticket_assign",
|
||||
field=models.BooleanField(
|
||||
default=helpdesk.models.email_on_ticket_assign_default,
|
||||
help_text="If you are assigned a ticket via the web, do you want to receive an e-mail?",
|
||||
verbose_name="E-mail me when assigned a ticket?",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='usersettings',
|
||||
name='email_on_ticket_change',
|
||||
field=models.BooleanField(default=helpdesk.models.email_on_ticket_change_default, help_text="If you're the ticket owner and the ticket is changed via the web by somebody else, do you want to receive an e-mail?", verbose_name='E-mail me on ticket change?'),
|
||||
model_name="usersettings",
|
||||
name="email_on_ticket_change",
|
||||
field=models.BooleanField(
|
||||
default=helpdesk.models.email_on_ticket_change_default,
|
||||
help_text="If you're the ticket owner and the ticket is changed via the web by somebody else, do you want to receive an e-mail?",
|
||||
verbose_name="E-mail me on ticket change?",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='usersettings',
|
||||
name='login_view_ticketlist',
|
||||
field=models.BooleanField(default=helpdesk.models.login_view_ticketlist_default, help_text='Display the ticket list upon login? Otherwise, the dashboard is shown.', verbose_name='Show Ticket List on Login?'),
|
||||
model_name="usersettings",
|
||||
name="login_view_ticketlist",
|
||||
field=models.BooleanField(
|
||||
default=helpdesk.models.login_view_ticketlist_default,
|
||||
help_text="Display the ticket list upon login? Otherwise, the dashboard is shown.",
|
||||
verbose_name="Show Ticket List on Login?",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='usersettings',
|
||||
name='tickets_per_page',
|
||||
field=models.IntegerField(choices=[(10, '10'), (25, '25'), (50, '50'), (100, '100')], default=helpdesk.models.tickets_per_page_default, help_text='How many tickets do you want to see on the Ticket List page?', verbose_name='Number of tickets to show per page'),
|
||||
model_name="usersettings",
|
||||
name="tickets_per_page",
|
||||
field=models.IntegerField(
|
||||
choices=[(10, "10"), (25, "25"), (50, "50"), (100, "100")],
|
||||
default=helpdesk.models.tickets_per_page_default,
|
||||
help_text="How many tickets do you want to see on the Ticket List page?",
|
||||
verbose_name="Number of tickets to show per page",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='usersettings',
|
||||
name='use_email_as_submitter',
|
||||
field=models.BooleanField(default=helpdesk.models.use_email_as_submitter_default, help_text='When you submit a ticket, do you want to automatically use your e-mail address as the submitter address? You can type a different e-mail address when entering the ticket if needed, this option only changes the default.', verbose_name='Use my e-mail address when submitting tickets?'),
|
||||
model_name="usersettings",
|
||||
name="use_email_as_submitter",
|
||||
field=models.BooleanField(
|
||||
default=helpdesk.models.use_email_as_submitter_default,
|
||||
help_text="When you submit a ticket, do you want to automatically use your e-mail address as the submitter address? You can type a different e-mail address when entering the ticket if needed, this option only changes the default.",
|
||||
verbose_name="Use my e-mail address when submitting tickets?",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='usersettings',
|
||||
name='settings_pickled',
|
||||
field=models.TextField(blank=True, help_text='DEPRECATED! This is a base64-encoded representation of a pickled Python dictionary. Do not change this field via the admin.', null=True, verbose_name='DEPRECATED! Settings Dictionary DEPRECATED!'),
|
||||
model_name="usersettings",
|
||||
name="settings_pickled",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
help_text="DEPRECATED! This is a base64-encoded representation of a pickled Python dictionary. Do not change this field via the admin.",
|
||||
null=True,
|
||||
verbose_name="DEPRECATED! Settings Dictionary DEPRECATED!",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(move_old_values),
|
||||
]
|
||||
|
@ -4,61 +4,105 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('helpdesk', '0020_depickle_user_settings'),
|
||||
("helpdesk", "0020_depickle_user_settings"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='kbitem',
|
||||
name='voted_by',
|
||||
model_name="kbitem",
|
||||
name="voted_by",
|
||||
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='followup',
|
||||
name='public',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.', verbose_name='Public'),
|
||||
model_name="followup",
|
||||
name="public",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.",
|
||||
verbose_name="Public",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ignoreemail',
|
||||
name='keep_in_mailbox',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.', verbose_name='Save Emails in Mailbox?'),
|
||||
model_name="ignoreemail",
|
||||
name="keep_in_mailbox",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.",
|
||||
verbose_name="Save Emails in Mailbox?",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='allow_email_submission',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Do you want to poll the e-mail box below for new tickets?', verbose_name='Allow E-Mail Submission?'),
|
||||
model_name="queue",
|
||||
name="allow_email_submission",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="Do you want to poll the e-mail box below for new tickets?",
|
||||
verbose_name="Allow E-Mail Submission?",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='allow_public_submission',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Should this queue be listed on the public submission form?', verbose_name='Allow Public Submission?'),
|
||||
model_name="queue",
|
||||
name="allow_public_submission",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="Should this queue be listed on the public submission form?",
|
||||
verbose_name="Allow Public Submission?",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='email_box_ssl',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.', verbose_name='Use SSL for E-Mail?'),
|
||||
model_name="queue",
|
||||
name="email_box_ssl",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.",
|
||||
verbose_name="Use SSL for E-Mail?",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='savedsearch',
|
||||
name='shared',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Should other users see this query?', verbose_name='Shared With Other Users?'),
|
||||
model_name="savedsearch",
|
||||
name="shared",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="Should other users see this query?",
|
||||
verbose_name="Shared With Other Users?",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticket',
|
||||
name='on_hold',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='If a ticket is on hold, it will not automatically be escalated.', verbose_name='On Hold'),
|
||||
model_name="ticket",
|
||||
name="on_hold",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="If a ticket is on hold, it will not automatically be escalated.",
|
||||
verbose_name="On Hold",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticketcc',
|
||||
name='can_update',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Can this CC login and update the ticket?', verbose_name='Can Update Ticket?'),
|
||||
model_name="ticketcc",
|
||||
name="can_update",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="Can this CC login and update the ticket?",
|
||||
verbose_name="Can Update Ticket?",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticketcc',
|
||||
name='can_view',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='Can this CC login to view the ticket details?', verbose_name='Can View Ticket?'),
|
||||
model_name="ticketcc",
|
||||
name="can_view",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="Can this CC login to view the ticket details?",
|
||||
verbose_name="Can View Ticket?",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -4,15 +4,21 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0021_voting_tracker'),
|
||||
("helpdesk", "0021_voting_tracker"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='followup',
|
||||
name='message_id',
|
||||
field=models.CharField(blank=True, editable=False, help_text="The Message ID of the submitter's email.", max_length=256, null=True, verbose_name='E-Mail ID'),
|
||||
model_name="followup",
|
||||
name="message_id",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
editable=False,
|
||||
help_text="The Message ID of the submitter's email.",
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name="E-Mail ID",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -4,15 +4,18 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0022_add_submitter_email_id_field_to_ticket'),
|
||||
("helpdesk", "0022_add_submitter_email_id_field_to_ticket"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='queue',
|
||||
name='enable_notifications_on_email_events',
|
||||
field=models.BooleanField(default=False, help_text='When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature', verbose_name='Notify contacts when email updates arrive'),
|
||||
model_name="queue",
|
||||
name="enable_notifications_on_email_events",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature",
|
||||
verbose_name="Notify contacts when email updates arrive",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -4,15 +4,16 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0023_add_enable_notifications_on_email_events_to_ticket'),
|
||||
("helpdesk", "0023_add_enable_notifications_on_email_events_to_ticket"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='followup',
|
||||
name='time_spent',
|
||||
field=models.DurationField(blank=True, help_text='Time spent on this follow up', null=True),
|
||||
model_name="followup",
|
||||
name="time_spent",
|
||||
field=models.DurationField(
|
||||
blank=True, help_text="Time spent on this follow up", null=True
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -4,15 +4,18 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0024_time_spent'),
|
||||
("helpdesk", "0024_time_spent"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='queue',
|
||||
name='dedicated_time',
|
||||
field=models.DurationField(blank=True, help_text='Time to be spent on this Queue in total', null=True),
|
||||
model_name="queue",
|
||||
name="dedicated_time",
|
||||
field=models.DurationField(
|
||||
blank=True,
|
||||
help_text="Time to be spent on this Queue in total",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -6,31 +6,63 @@ import helpdesk.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0025_queue_dedicated_time'),
|
||||
("helpdesk", "0025_queue_dedicated_time"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='KBIAttachment',
|
||||
name="KBIAttachment",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.FileField(max_length=1000, upload_to=helpdesk.models.attachment_path, verbose_name='File')),
|
||||
('filename', models.CharField(max_length=1000, verbose_name='Filename')),
|
||||
('mime_type', models.CharField(max_length=255, verbose_name='MIME Type')),
|
||||
('size', models.IntegerField(help_text='Size of this file in bytes', verbose_name='Size')),
|
||||
('kbitem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='helpdesk.KBItem', verbose_name='Knowledge base item')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"file",
|
||||
models.FileField(
|
||||
max_length=1000,
|
||||
upload_to=helpdesk.models.attachment_path,
|
||||
verbose_name="File",
|
||||
),
|
||||
),
|
||||
(
|
||||
"filename",
|
||||
models.CharField(max_length=1000, verbose_name="Filename"),
|
||||
),
|
||||
(
|
||||
"mime_type",
|
||||
models.CharField(max_length=255, verbose_name="MIME Type"),
|
||||
),
|
||||
(
|
||||
"size",
|
||||
models.IntegerField(
|
||||
help_text="Size of this file in bytes", verbose_name="Size"
|
||||
),
|
||||
),
|
||||
(
|
||||
"kbitem",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="helpdesk.KBItem",
|
||||
verbose_name="Knowledge base item",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Attachment',
|
||||
'verbose_name_plural': 'Attachments',
|
||||
'ordering': ('filename',),
|
||||
'abstract': False,
|
||||
"verbose_name": "Attachment",
|
||||
"verbose_name_plural": "Attachments",
|
||||
"ordering": ("filename",),
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='Attachment',
|
||||
new_name='FollowUpAttachment',
|
||||
old_name="Attachment",
|
||||
new_name="FollowUpAttachment",
|
||||
),
|
||||
]
|
||||
|
@ -6,66 +6,98 @@ import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('helpdesk', '0026_kbitem_attachments'),
|
||||
("helpdesk", "0026_kbitem_attachments"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='kbcategory',
|
||||
name='queue',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='helpdesk.Queue', verbose_name='Default queue when creating a ticket after viewing this category.'),
|
||||
model_name="kbcategory",
|
||||
name="queue",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="helpdesk.Queue",
|
||||
verbose_name="Default queue when creating a ticket after viewing this category.",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='kbitem',
|
||||
name='downvoted_by',
|
||||
field=models.ManyToManyField(related_name='downvotes', to=settings.AUTH_USER_MODEL),
|
||||
model_name="kbitem",
|
||||
name="downvoted_by",
|
||||
field=models.ManyToManyField(
|
||||
related_name="downvotes", to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='kbitem',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='helpdesk.KBItem', verbose_name='Knowledge base item the user was viewing when they created this ticket.'),
|
||||
model_name="ticket",
|
||||
name="kbitem",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="helpdesk.KBItem",
|
||||
verbose_name="Knowledge base item the user was viewing when they created this ticket.",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='followupattachment',
|
||||
name='filename',
|
||||
field=models.CharField(blank=True, max_length=1000, verbose_name='Filename'),
|
||||
model_name="followupattachment",
|
||||
name="filename",
|
||||
field=models.CharField(
|
||||
blank=True, max_length=1000, verbose_name="Filename"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='followupattachment',
|
||||
name='mime_type',
|
||||
field=models.CharField(blank=True, max_length=255, verbose_name='MIME Type'),
|
||||
model_name="followupattachment",
|
||||
name="mime_type",
|
||||
field=models.CharField(
|
||||
blank=True, max_length=255, verbose_name="MIME Type"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='followupattachment',
|
||||
name='size',
|
||||
field=models.IntegerField(blank=True, help_text='Size of this file in bytes', verbose_name='Size'),
|
||||
model_name="followupattachment",
|
||||
name="size",
|
||||
field=models.IntegerField(
|
||||
blank=True, help_text="Size of this file in bytes", verbose_name="Size"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='kbiattachment',
|
||||
name='filename',
|
||||
field=models.CharField(blank=True, max_length=1000, verbose_name='Filename'),
|
||||
model_name="kbiattachment",
|
||||
name="filename",
|
||||
field=models.CharField(
|
||||
blank=True, max_length=1000, verbose_name="Filename"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='kbiattachment',
|
||||
name='mime_type',
|
||||
field=models.CharField(blank=True, max_length=255, verbose_name='MIME Type'),
|
||||
model_name="kbiattachment",
|
||||
name="mime_type",
|
||||
field=models.CharField(
|
||||
blank=True, max_length=255, verbose_name="MIME Type"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='kbiattachment',
|
||||
name='size',
|
||||
field=models.IntegerField(blank=True, help_text='Size of this file in bytes', verbose_name='Size'),
|
||||
model_name="kbiattachment",
|
||||
name="size",
|
||||
field=models.IntegerField(
|
||||
blank=True, help_text="Size of this file in bytes", verbose_name="Size"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='kbitem',
|
||||
name='voted_by',
|
||||
field=models.ManyToManyField(related_name='votes', to=settings.AUTH_USER_MODEL),
|
||||
model_name="kbitem",
|
||||
name="voted_by",
|
||||
field=models.ManyToManyField(
|
||||
related_name="votes", to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='enable_notifications_on_email_events',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature', verbose_name='Notify contacts when email updates arrive'),
|
||||
model_name="queue",
|
||||
name="enable_notifications_on_email_events",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text="When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature",
|
||||
verbose_name="Notify contacts when email updates arrive",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -7,15 +7,20 @@ from helpdesk import settings as helpdesk_settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0027_auto_20200107_1221'),
|
||||
("helpdesk", "0027_auto_20200107_1221"),
|
||||
] + helpdesk_settings.HELPDESK_TEAMS_MIGRATION_DEPENDENCIES
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='kbitem',
|
||||
name='team',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=helpdesk_settings.HELPDESK_TEAMS_MODEL, verbose_name='Team'),
|
||||
model_name="kbitem",
|
||||
name="team",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=helpdesk_settings.HELPDESK_TEAMS_MODEL,
|
||||
verbose_name="Team",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -4,15 +4,16 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0028_kbitem_team'),
|
||||
("helpdesk", "0028_kbitem_team"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='kbcategory',
|
||||
name='public',
|
||||
field=models.BooleanField(default=True, verbose_name='Is KBCategory publicly visible?'),
|
||||
model_name="kbcategory",
|
||||
name="public",
|
||||
field=models.BooleanField(
|
||||
default=True, verbose_name="Is KBCategory publicly visible?"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -2,32 +2,44 @@
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def copy_title(apps, schema_editor):
|
||||
KBCategory = apps.get_model("helpdesk", "KBCategory")
|
||||
KBCategory.objects.update(name=models.F('title'))
|
||||
KBCategory.objects.update(name=models.F("title"))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0029_kbcategory_public'),
|
||||
("helpdesk", "0029_kbcategory_public"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='kbcategory',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Name of the category'),
|
||||
model_name="kbcategory",
|
||||
name="name",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
max_length=100,
|
||||
null=True,
|
||||
verbose_name="Name of the category",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='kbcategory',
|
||||
name='title',
|
||||
field=models.CharField(max_length=100, verbose_name='Title on knowledgebase page'),
|
||||
model_name="kbcategory",
|
||||
name="title",
|
||||
field=models.CharField(
|
||||
max_length=100, verbose_name="Title on knowledgebase page"
|
||||
),
|
||||
),
|
||||
migrations.RunPython(copy_title, migrations.RunPython.noop),
|
||||
migrations.AlterField(
|
||||
model_name='kbcategory',
|
||||
name='name',
|
||||
field=models.CharField(blank=False, max_length=100, null=False, verbose_name='Name of the category'),
|
||||
model_name="kbcategory",
|
||||
name="name",
|
||||
field=models.CharField(
|
||||
blank=False,
|
||||
max_length=100,
|
||||
null=False,
|
||||
verbose_name="Name of the category",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -4,19 +4,24 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0030_add_kbcategory_name'),
|
||||
("helpdesk", "0030_add_kbcategory_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='kbitem',
|
||||
options={'ordering': ('order', 'title'), 'verbose_name': 'Knowledge base item', 'verbose_name_plural': 'Knowledge base items'},
|
||||
name="kbitem",
|
||||
options={
|
||||
"ordering": ("order", "title"),
|
||||
"verbose_name": "Knowledge base item",
|
||||
"verbose_name_plural": "Knowledge base items",
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='kbitem',
|
||||
name='order',
|
||||
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Order'),
|
||||
model_name="kbitem",
|
||||
name="order",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, null=True, verbose_name="Order"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -4,15 +4,16 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0031_auto_20200225_1440'),
|
||||
("helpdesk", "0031_auto_20200225_1440"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='kbitem',
|
||||
name='enabled',
|
||||
field=models.BooleanField(default=True, verbose_name='Enabled to display to users'),
|
||||
model_name="kbitem",
|
||||
name="enabled",
|
||||
field=models.BooleanField(
|
||||
default=True, verbose_name="Enabled to display to users"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -5,15 +5,21 @@ import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0032_kbitem_enabled'),
|
||||
("helpdesk", "0032_kbitem_enabled"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='merged_to',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='merged_tickets', to='helpdesk.Ticket', verbose_name='merged to'),
|
||||
model_name="ticket",
|
||||
name="merged_to",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="merged_tickets",
|
||||
to="helpdesk.Ticket",
|
||||
verbose_name="merged to",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -7,10 +7,12 @@ def forwards_func(apps, schema_editor):
|
||||
EmailTemplate = apps.get_model("helpdesk", "EmailTemplate")
|
||||
db_alias = schema_editor.connection.alias
|
||||
EmailTemplate.objects.using(db_alias).create(
|
||||
id=EmailTemplate.objects.using(db_alias).order_by('-id').first().id + 1 if EmailTemplate.objects.using(db_alias).first() else 1, # because PG sequences are not reset
|
||||
template_name='merged',
|
||||
subject='(Merged)',
|
||||
heading='Ticket merged',
|
||||
id=EmailTemplate.objects.using(db_alias).order_by("-id").first().id + 1
|
||||
if EmailTemplate.objects.using(db_alias).first()
|
||||
else 1, # because PG sequences are not reset
|
||||
template_name="merged",
|
||||
subject="(Merged)",
|
||||
heading="Ticket merged",
|
||||
plain_text="""Hello,
|
||||
|
||||
This is a courtesy e-mail to let you know that ticket {{ ticket.ticket }} ("{{ ticket.title }}") by {{ ticket.submitter_email }} has been merged to ticket {{ ticket.merged_to.ticket }}.
|
||||
@ -21,13 +23,14 @@ From now on, please answer on this ticket, or you can include the tag {{ ticket.
|
||||
<p style="font-family: sans-serif; font-size: 1em;">This is a courtesy e-mail to let you know that ticket <b>{{ ticket.ticket }}</b> (<em>{{ ticket.title }}</em>) by {{ ticket.submitter_email }} has been merged to ticket <a href="{{ ticket.merged_to.staff_url }}">{{ ticket.merged_to.ticket }}</a>.</p>
|
||||
|
||||
<p style="font-family: sans-serif; font-size: 1em;">From now on, please answer on this ticket, or you can include the tag <b>{{ ticket.merged_to.ticket }}</b> in your e-mail subject.</p>""",
|
||||
locale='en'
|
||||
locale="en",
|
||||
)
|
||||
EmailTemplate.objects.using(db_alias).create(
|
||||
id=EmailTemplate.objects.using(db_alias).order_by('-id').first().id + 1, # because PG sequences are not reset
|
||||
template_name='merged',
|
||||
subject='(Fusionné)',
|
||||
heading='Ticket Fusionné',
|
||||
id=EmailTemplate.objects.using(db_alias).order_by("-id").first().id
|
||||
+ 1, # because PG sequences are not reset
|
||||
template_name="merged",
|
||||
subject="(Fusionné)",
|
||||
heading="Ticket Fusionné",
|
||||
plain_text="""Bonjour,
|
||||
|
||||
Ce courriel indicatif permet de vous prévenir que le ticket {{ ticket.ticket }} ("{{ ticket.title }}") par {{ ticket.submitter_email }} a été fusionné au ticket {{ ticket.merged_to.ticket }}.
|
||||
@ -38,20 +41,19 @@ Veillez à répondre sur ce ticket dorénavant, ou bien inclure la balise {{ tic
|
||||
<p style="font-family: sans-serif; font-size: 1em;">Ce courriel indicatif permet de vous prévenir que le ticket <b>{{ ticket.ticket }}</b> (<em>{{ ticket.title }}</em>) par {{ ticket.submitter_email }} a été fusionné au ticket <a href="{{ ticket.merged_to.staff_url }}">{{ ticket.merged_to.ticket }}</a>.</p>
|
||||
|
||||
<p style="font-family: sans-serif; font-size: 1em;">Veillez à répondre sur ce ticket dorénavant, ou bien inclure la balise <b>{{ ticket.merged_to.ticket }}</b> dans le sujet de votre réponse par mail.</p>""",
|
||||
locale='fr'
|
||||
locale="fr",
|
||||
)
|
||||
|
||||
|
||||
def reverse_func(apps, schema_editor):
|
||||
EmailTemplate = apps.get_model("helpdesk", "EmailTemplate")
|
||||
db_alias = schema_editor.connection.alias
|
||||
EmailTemplate.objects.using(db_alias).filter(template_name='merged').delete()
|
||||
EmailTemplate.objects.using(db_alias).filter(template_name="merged").delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0033_ticket_merged_to'),
|
||||
("helpdesk", "0033_ticket_merged_to"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
@ -5,15 +5,18 @@ import helpdesk.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("helpdesk", "0034_create_email_template_for_merged"),
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0034_create_email_template_for_merged'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='usersettings',
|
||||
name='email_on_ticket_change',
|
||||
field=models.BooleanField(default=helpdesk.models.email_on_ticket_change_default, help_text="If you're the ticket owner and the ticket is changed via the web by somebody else,do you want to receive an e-mail?", verbose_name='E-mail me on ticket change?'),
|
||||
model_name="usersettings",
|
||||
name="email_on_ticket_change",
|
||||
field=models.BooleanField(
|
||||
default=helpdesk.models.email_on_ticket_change_default,
|
||||
help_text="If you're the ticket owner and the ticket is changed via the web by somebody else,do you want to receive an e-mail?",
|
||||
verbose_name="E-mail me on ticket change?",
|
||||
),
|
||||
),
|
||||
]
|
||||
]
|
||||
|
@ -6,20 +6,29 @@ import helpdesk.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0035_alter_email_on_ticket_change'),
|
||||
("helpdesk", "0035_alter_email_on_ticket_change"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='followupattachment',
|
||||
name='file',
|
||||
field=models.FileField(max_length=1000, upload_to=helpdesk.models.attachment_path, validators=[helpdesk.validators.validate_file_extension], verbose_name='File'),
|
||||
model_name="followupattachment",
|
||||
name="file",
|
||||
field=models.FileField(
|
||||
max_length=1000,
|
||||
upload_to=helpdesk.models.attachment_path,
|
||||
validators=[helpdesk.validators.validate_file_extension],
|
||||
verbose_name="File",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='kbiattachment',
|
||||
name='file',
|
||||
field=models.FileField(max_length=1000, upload_to=helpdesk.models.attachment_path, validators=[helpdesk.validators.validate_file_extension], verbose_name='File'),
|
||||
model_name="kbiattachment",
|
||||
name="file",
|
||||
field=models.FileField(
|
||||
max_length=1000,
|
||||
upload_to=helpdesk.models.attachment_path,
|
||||
validators=[helpdesk.validators.validate_file_extension],
|
||||
verbose_name="File",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -4,15 +4,26 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0036_add_attachment_validator'),
|
||||
("helpdesk", "0036_add_attachment_validator"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='email_box_type',
|
||||
field=models.CharField(blank=True, choices=[('pop3', 'POP 3'), ('imap', 'IMAP'), ('oauth', 'IMAP OAUTH'), ('local', 'Local Directory')], help_text='E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.', max_length=5, null=True, verbose_name='E-Mail Box Type'),
|
||||
model_name="queue",
|
||||
name="email_box_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("pop3", "POP 3"),
|
||||
("imap", "IMAP"),
|
||||
("oauth", "IMAP OAUTH"),
|
||||
("local", "Local Directory"),
|
||||
],
|
||||
help_text="E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.",
|
||||
max_length=5,
|
||||
null=True,
|
||||
verbose_name="E-Mail Box Type",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -6,49 +6,107 @@ import helpdesk.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0037_alter_queue_email_box_type'),
|
||||
("helpdesk", "0037_alter_queue_email_box_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Checklist',
|
||||
name="Checklist",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='Name')),
|
||||
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to='helpdesk.ticket', verbose_name='Ticket')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=100, verbose_name="Name")),
|
||||
(
|
||||
"ticket",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="checklists",
|
||||
to="helpdesk.ticket",
|
||||
verbose_name="Ticket",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Checklist',
|
||||
'verbose_name_plural': 'Checklists',
|
||||
"verbose_name": "Checklist",
|
||||
"verbose_name_plural": "Checklists",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ChecklistTemplate',
|
||||
name="ChecklistTemplate",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='Name')),
|
||||
('task_list', models.JSONField(validators=[helpdesk.models.is_a_list_without_empty_element], verbose_name='Task List')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=100, verbose_name="Name")),
|
||||
(
|
||||
"task_list",
|
||||
models.JSONField(
|
||||
validators=[helpdesk.models.is_a_list_without_empty_element],
|
||||
verbose_name="Task List",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Checklist Template',
|
||||
'verbose_name_plural': 'Checklist Templates',
|
||||
"verbose_name": "Checklist Template",
|
||||
"verbose_name_plural": "Checklist Templates",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ChecklistTask',
|
||||
name="ChecklistTask",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('description', models.CharField(max_length=250, verbose_name='Description')),
|
||||
('completion_date', models.DateTimeField(blank=True, null=True, verbose_name='Completion Date')),
|
||||
('position', models.PositiveSmallIntegerField(db_index=True, verbose_name='Position')),
|
||||
('checklist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='helpdesk.checklist', verbose_name='Checklist')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"description",
|
||||
models.CharField(max_length=250, verbose_name="Description"),
|
||||
),
|
||||
(
|
||||
"completion_date",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="Completion Date"
|
||||
),
|
||||
),
|
||||
(
|
||||
"position",
|
||||
models.PositiveSmallIntegerField(
|
||||
db_index=True, verbose_name="Position"
|
||||
),
|
||||
),
|
||||
(
|
||||
"checklist",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="tasks",
|
||||
to="helpdesk.checklist",
|
||||
verbose_name="Checklist",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Checklist Task',
|
||||
'verbose_name_plural': 'Checklist Tasks',
|
||||
'ordering': ('position',),
|
||||
"verbose_name": "Checklist Task",
|
||||
"verbose_name_plural": "Checklist Tasks",
|
||||
"ordering": ("position",),
|
||||
},
|
||||
),
|
||||
]
|
||||
|
1205
helpdesk/models.py
1205
helpdesk/models.py
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,3 @@
|
||||
|
||||
from base64 import b64decode, b64encode
|
||||
from django.db.models import Q, Max
|
||||
from django.db.models import F, Window, Subquery, OuterRef
|
||||
@ -15,61 +14,61 @@ 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")
|
||||
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'] = ''
|
||||
query = {"search_string": ""}
|
||||
query.update(json.loads(b64decode(b64data).decode("utf-8")))
|
||||
if query["search_string"] is None:
|
||||
query["search_string"] = ""
|
||||
return query
|
||||
|
||||
|
||||
def get_search_filter_args(search):
|
||||
if not search:
|
||||
return Q()
|
||||
if search.startswith('queue:'):
|
||||
return Q(queue__title__icontains=search[len('queue:'):])
|
||||
if search.startswith('priority:'):
|
||||
return Q(priority__icontains=search[len('priority:'):])
|
||||
if search.startswith("queue:"):
|
||||
return Q(queue__title__icontains=search[len("queue:") :])
|
||||
if search.startswith("priority:"):
|
||||
return Q(priority__icontains=search[len("priority:") :])
|
||||
my_filter = Q()
|
||||
for subsearch in search.split("OR"):
|
||||
subsearch = subsearch.strip()
|
||||
if not subsearch:
|
||||
continue
|
||||
my_filter = (
|
||||
filter |
|
||||
Q(id__icontains=subsearch) |
|
||||
Q(title__icontains=subsearch) |
|
||||
Q(description__icontains=subsearch) |
|
||||
Q(priority__icontains=subsearch) |
|
||||
Q(resolution__icontains=subsearch) |
|
||||
Q(submitter_email__icontains=subsearch) |
|
||||
Q(assigned_to__email__icontains=subsearch) |
|
||||
Q(ticketcustomfieldvalue__value__icontains=subsearch) |
|
||||
Q(created__icontains=subsearch) |
|
||||
Q(due_date__icontains=subsearch)
|
||||
filter
|
||||
| Q(id__icontains=subsearch)
|
||||
| Q(title__icontains=subsearch)
|
||||
| Q(description__icontains=subsearch)
|
||||
| Q(priority__icontains=subsearch)
|
||||
| Q(resolution__icontains=subsearch)
|
||||
| Q(submitter_email__icontains=subsearch)
|
||||
| Q(assigned_to__email__icontains=subsearch)
|
||||
| Q(ticketcustomfieldvalue__value__icontains=subsearch)
|
||||
| Q(created__icontains=subsearch)
|
||||
| Q(due_date__icontains=subsearch)
|
||||
)
|
||||
return my_filter
|
||||
|
||||
|
||||
DATATABLES_ORDER_COLUMN_CHOICES = Choices(
|
||||
('0', 'id'),
|
||||
('1', 'title'),
|
||||
('2', 'priority'),
|
||||
('3', 'queue'),
|
||||
('4', 'status'),
|
||||
('5', 'created'),
|
||||
('6', 'due_date'),
|
||||
('7', 'assigned_to'),
|
||||
('8', 'submitter_email'),
|
||||
('9', 'last_followup'),
|
||||
("0", "id"),
|
||||
("1", "title"),
|
||||
("2", "priority"),
|
||||
("3", "queue"),
|
||||
("4", "status"),
|
||||
("5", "created"),
|
||||
("6", "due_date"),
|
||||
("7", "assigned_to"),
|
||||
("8", "submitter_email"),
|
||||
("9", "last_followup"),
|
||||
# ('10', 'time_spent'),
|
||||
('11', 'kbitem'),
|
||||
("11", "kbitem"),
|
||||
)
|
||||
|
||||
|
||||
@ -78,22 +77,19 @@ def get_query_class():
|
||||
|
||||
def _get_query_class():
|
||||
return __Query__
|
||||
return getattr(settings,
|
||||
'HELPDESK_QUERY_CLASS',
|
||||
_get_query_class)()
|
||||
|
||||
return getattr(settings, "HELPDESK_QUERY_CLASS", _get_query_class)()
|
||||
|
||||
|
||||
class __Query__:
|
||||
def __init__(self, huser, base64query=None, query_params=None):
|
||||
self.huser = huser
|
||||
self.params = query_params if query_params else query_from_base64(
|
||||
base64query)
|
||||
self.base64 = base64query if base64query else query_to_base64(
|
||||
query_params)
|
||||
self.params = query_params if query_params else query_from_base64(base64query)
|
||||
self.base64 = base64query if base64query else query_to_base64(query_params)
|
||||
self.result = None
|
||||
|
||||
def get_search_filter_args(self):
|
||||
search = self.params.get('search_string', '')
|
||||
search = self.params.get("search_string", "")
|
||||
return get_search_filter_args(search)
|
||||
|
||||
def __run__(self, queryset):
|
||||
@ -112,15 +108,15 @@ class __Query__:
|
||||
sorting: The name of the column to sort by
|
||||
"""
|
||||
q_args = []
|
||||
value_filters = self.params.get('filtering', {})
|
||||
null_filters = self.params.get('filtering_null', {})
|
||||
value_filters = self.params.get("filtering", {})
|
||||
null_filters = self.params.get("filtering_null", {})
|
||||
if null_filters:
|
||||
if value_filters:
|
||||
# Check if any of the value value_filters are for the same field as the
|
||||
# ISNULL filter so that an OR filter can be set up
|
||||
matched_null_keys = []
|
||||
for null_key in null_filters:
|
||||
field_path = null_key[:-8] # Chop off the "__isnull"
|
||||
field_path = null_key[:-8] # Chop off the "__isnull"
|
||||
matched_key = None
|
||||
for val_key in value_filters:
|
||||
if val_key.startswith(field_path):
|
||||
@ -140,10 +136,12 @@ class __Query__:
|
||||
for null_key in matched_null_keys:
|
||||
del null_filters[null_key]
|
||||
queryset = queryset.filter(
|
||||
*q_args, (Q(**value_filters) & Q(**null_filters)) & self.get_search_filter_args())
|
||||
sorting = self.params.get('sorting', None)
|
||||
*q_args,
|
||||
(Q(**value_filters) & Q(**null_filters)) & self.get_search_filter_args(),
|
||||
)
|
||||
sorting = self.params.get("sorting", None)
|
||||
if sorting:
|
||||
sortreverse = self.params.get('sortreverse', None)
|
||||
sortreverse = self.params.get("sortreverse", None)
|
||||
if sortreverse:
|
||||
sorting = "-%s" % sorting
|
||||
queryset = queryset.order_by(sorting)
|
||||
@ -165,45 +163,49 @@ class __Query__:
|
||||
to a Serializer called DatatablesTicketSerializer in serializers.py.
|
||||
"""
|
||||
objects = self.get()
|
||||
order_by = '-created'
|
||||
draw = int(kwargs.get('draw', [0])[0])
|
||||
length = int(kwargs.get('length', [25])[0])
|
||||
start = int(kwargs.get('start', [0])[0])
|
||||
search_value = kwargs.get('search[value]', [""])[0]
|
||||
order_column = kwargs.get('order[0][column]', ['5'])[0]
|
||||
order = kwargs.get('order[0][dir]', ["asc"])[0]
|
||||
|
||||
order_by = "-created"
|
||||
draw = int(kwargs.get("draw", [0])[0])
|
||||
length = int(kwargs.get("length", [25])[0])
|
||||
start = int(kwargs.get("start", [0])[0])
|
||||
search_value = kwargs.get("search[value]", [""])[0]
|
||||
order_column = kwargs.get("order[0][column]", ["5"])[0]
|
||||
order = kwargs.get("order[0][dir]", ["asc"])[0]
|
||||
|
||||
order_column = DATATABLES_ORDER_COLUMN_CHOICES[order_column]
|
||||
# django orm '-' -> desc
|
||||
if order == 'desc':
|
||||
order_column = '-' + order_column
|
||||
|
||||
if order == "desc":
|
||||
order_column = "-" + order_column
|
||||
|
||||
queryset = objects.annotate(
|
||||
last_followup=Subquery(
|
||||
FollowUp.objects.order_by().annotate(
|
||||
FollowUp.objects.order_by()
|
||||
.annotate(
|
||||
last_followup=Window(
|
||||
expression=Max("date"),
|
||||
partition_by=[F("ticket_id"),],
|
||||
order_by="-date"
|
||||
partition_by=[
|
||||
F("ticket_id"),
|
||||
],
|
||||
order_by="-date",
|
||||
)
|
||||
).filter(
|
||||
ticket_id=OuterRef("id")
|
||||
).values("last_followup").distinct()
|
||||
)
|
||||
.filter(ticket_id=OuterRef("id"))
|
||||
.values("last_followup")
|
||||
.distinct()
|
||||
)
|
||||
).order_by(order_by)
|
||||
|
||||
|
||||
total = queryset.count()
|
||||
|
||||
if search_value: # Dead code currently
|
||||
queryset = queryset.filter(get_search_filter_args(search_value))
|
||||
|
||||
count = queryset.count()
|
||||
queryset = queryset.order_by(order_column)[start:start + length]
|
||||
queryset = queryset.order_by(order_column)[start : start + length]
|
||||
return {
|
||||
'data': DatatablesTicketSerializer(queryset, many=True).data,
|
||||
'recordsFiltered': count,
|
||||
'recordsTotal': total,
|
||||
'draw': draw
|
||||
"data": DatatablesTicketSerializer(queryset, many=True).data,
|
||||
"recordsFiltered": count,
|
||||
"recordsTotal": total,
|
||||
"draw": draw,
|
||||
}
|
||||
|
||||
def get_timeline_context(self):
|
||||
@ -212,33 +214,38 @@ class __Query__:
|
||||
for ticket in self.get():
|
||||
for followup in ticket.followup_set.all():
|
||||
event = {
|
||||
'start_date': self.mk_timeline_date(followup.date),
|
||||
'text': {
|
||||
'headline': ticket.title + ' - ' + followup.title,
|
||||
'text': (
|
||||
(escape(followup.comment)
|
||||
if followup.comment else _('No text'))
|
||||
+
|
||||
'<br/> <a href="%s" class="btn" role="button">%s</a>'
|
||||
%
|
||||
(reverse('helpdesk:view', kwargs={
|
||||
'ticket_id': ticket.pk}), _("View ticket"))
|
||||
"start_date": self.mk_timeline_date(followup.date),
|
||||
"text": {
|
||||
"headline": ticket.title + " - " + followup.title,
|
||||
"text": (
|
||||
(
|
||||
escape(followup.comment)
|
||||
if followup.comment
|
||||
else _("No text")
|
||||
)
|
||||
+ '<br/> <a href="%s" class="btn" role="button">%s</a>'
|
||||
% (
|
||||
reverse(
|
||||
"helpdesk:view", kwargs={"ticket_id": ticket.pk}
|
||||
),
|
||||
_("View ticket"),
|
||||
)
|
||||
),
|
||||
},
|
||||
'group': _('Messages'),
|
||||
"group": _("Messages"),
|
||||
}
|
||||
events.append(event)
|
||||
|
||||
return {
|
||||
'events': events,
|
||||
"events": events,
|
||||
}
|
||||
|
||||
def mk_timeline_date(self, date):
|
||||
return {
|
||||
'year': date.year,
|
||||
'month': date.month,
|
||||
'day': date.day,
|
||||
'hour': date.hour,
|
||||
'minute': date.minute,
|
||||
'second': date.second,
|
||||
"year": date.year,
|
||||
"month": date.month,
|
||||
"day": date.day,
|
||||
"hour": date.hour,
|
||||
"minute": date.minute,
|
||||
"second": date.second,
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
|
||||
A serializer for the Ticket model, returns data in the format as required by
|
||||
datatables for ticket_list.html. Called from staff.datatables_ticket_list.
|
||||
"""
|
||||
|
||||
ticket = serializers.SerializerMethodField()
|
||||
assigned_to = serializers.SerializerMethodField()
|
||||
submitter = serializers.SerializerMethodField()
|
||||
@ -30,9 +31,22 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Ticket
|
||||
# fields = '__all__'
|
||||
fields = ('ticket', 'id', 'priority', 'title', 'queue', 'status',
|
||||
'created', 'due_date', 'assigned_to', 'submitter', 'last_followup',
|
||||
'row_class', 'time_spent', 'kbitem')
|
||||
fields = (
|
||||
"ticket",
|
||||
"id",
|
||||
"priority",
|
||||
"title",
|
||||
"queue",
|
||||
"status",
|
||||
"created",
|
||||
"due_date",
|
||||
"assigned_to",
|
||||
"submitter",
|
||||
"last_followup",
|
||||
"row_class",
|
||||
"time_spent",
|
||||
"kbitem",
|
||||
)
|
||||
|
||||
def get_queue(self, obj):
|
||||
return {"title": obj.queue.title, "id": obj.queue.id}
|
||||
@ -71,39 +85,46 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
|
||||
|
||||
def get_kbitem(self, obj):
|
||||
return obj.kbitem.title if obj.kbitem else ""
|
||||
|
||||
|
||||
def get_last_followup(self, obj):
|
||||
return obj.last_followup
|
||||
|
||||
|
||||
|
||||
|
||||
class FollowUpAttachmentSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FollowUpAttachment
|
||||
fields = ('id', 'followup', 'file', 'filename', 'mime_type', 'size')
|
||||
fields = ("id", "followup", "file", "filename", "mime_type", "size")
|
||||
|
||||
|
||||
class FollowUpSerializer(serializers.ModelSerializer):
|
||||
followupattachment_set = FollowUpAttachmentSerializer(
|
||||
many=True, read_only=True)
|
||||
followupattachment_set = FollowUpAttachmentSerializer(many=True, read_only=True)
|
||||
attachments = serializers.ListField(
|
||||
child=serializers.FileField(),
|
||||
write_only=True,
|
||||
required=False
|
||||
child=serializers.FileField(), write_only=True, required=False
|
||||
)
|
||||
date = serializers.DateTimeField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = FollowUp
|
||||
fields = (
|
||||
'id', 'ticket', 'user', 'title', 'comment', 'public', 'new_status',
|
||||
'time_spent', 'attachments', 'followupattachment_set', 'date', 'message_id',
|
||||
"id",
|
||||
"ticket",
|
||||
"user",
|
||||
"title",
|
||||
"comment",
|
||||
"public",
|
||||
"new_status",
|
||||
"time_spent",
|
||||
"attachments",
|
||||
"followupattachment_set",
|
||||
"date",
|
||||
"message_id",
|
||||
)
|
||||
|
||||
def create(self, validated_data):
|
||||
if validated_data["user"]:
|
||||
user = validated_data["user"]
|
||||
else:
|
||||
user = self.context['request'].user
|
||||
user = self.context["request"].user
|
||||
return update_ticket(
|
||||
user=user,
|
||||
ticket=validated_data["ticket"],
|
||||
@ -121,12 +142,12 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = get_user_model()
|
||||
fields = ('first_name', 'last_name', 'username', 'email', 'password')
|
||||
fields = ("first_name", "last_name", "username", "email", "password")
|
||||
|
||||
def create(self, validated_data):
|
||||
user = super(UserSerializer, self).create(validated_data)
|
||||
user.is_active = True
|
||||
user.set_password(validated_data['password'])
|
||||
user.set_password(validated_data["password"])
|
||||
user.save()
|
||||
return user
|
||||
|
||||
@ -137,13 +158,14 @@ class BaseTicketSerializer(serializers.ModelSerializer):
|
||||
|
||||
# Add custom fields
|
||||
for field in CustomField.objects.all():
|
||||
self.fields['custom_%s' % field.name] = field.build_api_field()
|
||||
self.fields["custom_%s" % field.name] = field.build_api_field()
|
||||
|
||||
|
||||
class PublicTicketListingSerializer(BaseTicketSerializer):
|
||||
"""
|
||||
A serializer to be used by the public API for listing tickets. Don't expose private fields here!
|
||||
"""
|
||||
|
||||
ticket = serializers.SerializerMethodField()
|
||||
submitter = serializers.SerializerMethodField()
|
||||
created = serializers.SerializerMethodField()
|
||||
@ -156,8 +178,18 @@ class PublicTicketListingSerializer(BaseTicketSerializer):
|
||||
class Meta:
|
||||
model = Ticket
|
||||
# fields = '__all__'
|
||||
fields = ('ticket', 'id', 'title', 'queue', 'status',
|
||||
'created', 'due_date', 'submitter', 'kbitem', 'secret_key')
|
||||
fields = (
|
||||
"ticket",
|
||||
"id",
|
||||
"title",
|
||||
"queue",
|
||||
"status",
|
||||
"created",
|
||||
"due_date",
|
||||
"submitter",
|
||||
"kbitem",
|
||||
"secret_key",
|
||||
)
|
||||
|
||||
def get_queue(self, obj):
|
||||
return {"title": obj.queue.title, "id": obj.queue.id}
|
||||
@ -188,29 +220,40 @@ class TicketSerializer(BaseTicketSerializer):
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = (
|
||||
'id', 'queue', 'title', 'description', 'resolution', 'submitter_email', 'assigned_to', 'status', 'on_hold',
|
||||
'priority', 'due_date', 'merged_to', 'attachment', 'followup_set'
|
||||
"id",
|
||||
"queue",
|
||||
"title",
|
||||
"description",
|
||||
"resolution",
|
||||
"submitter_email",
|
||||
"assigned_to",
|
||||
"status",
|
||||
"on_hold",
|
||||
"priority",
|
||||
"due_date",
|
||||
"merged_to",
|
||||
"attachment",
|
||||
"followup_set",
|
||||
)
|
||||
|
||||
def create(self, validated_data):
|
||||
""" Use TicketForm to validate and create ticket """
|
||||
queues = HelpdeskUser(self.context['request'].user).get_queues()
|
||||
"""Use TicketForm to validate and create ticket"""
|
||||
queues = HelpdeskUser(self.context["request"].user).get_queues()
|
||||
queue_choices = [(q.id, q.title) for q in queues]
|
||||
data = validated_data.copy()
|
||||
data['body'] = data['description']
|
||||
data["body"] = data["description"]
|
||||
# TicketForm needs id for ForeignKey (not the instance themselves)
|
||||
data['queue'] = data['queue'].id
|
||||
if data.get('assigned_to'):
|
||||
data['assigned_to'] = data['assigned_to'].id
|
||||
if data.get('merged_to'):
|
||||
data['merged_to'] = data['merged_to'].id
|
||||
data["queue"] = data["queue"].id
|
||||
if data.get("assigned_to"):
|
||||
data["assigned_to"] = data["assigned_to"].id
|
||||
if data.get("merged_to"):
|
||||
data["merged_to"] = data["merged_to"].id
|
||||
|
||||
files = {'attachment': data.pop('attachment', None)}
|
||||
files = {"attachment": data.pop("attachment", None)}
|
||||
|
||||
ticket_form = TicketForm(
|
||||
data=data, files=files, queue_choices=queue_choices)
|
||||
ticket_form = TicketForm(data=data, files=files, queue_choices=queue_choices)
|
||||
if ticket_form.is_valid():
|
||||
ticket = ticket_form.save(user=self.context['request'].user)
|
||||
ticket = ticket_form.save(user=self.context["request"].user)
|
||||
ticket.set_custom_field_values()
|
||||
return ticket
|
||||
|
||||
|
@ -14,11 +14,11 @@ import sys
|
||||
|
||||
|
||||
DEFAULT_USER_SETTINGS = {
|
||||
'login_view_ticketlist': True,
|
||||
'email_on_ticket_change': True,
|
||||
'email_on_ticket_assign': True,
|
||||
'tickets_per_page': 25,
|
||||
'use_email_as_submitter': True,
|
||||
"login_view_ticketlist": True,
|
||||
"email_on_ticket_change": True,
|
||||
"email_on_ticket_assign": True,
|
||||
"tickets_per_page": 25,
|
||||
"use_email_as_submitter": True,
|
||||
}
|
||||
|
||||
try:
|
||||
@ -33,8 +33,8 @@ HAS_TAG_SUPPORT = False
|
||||
USE_TZ: bool = True
|
||||
|
||||
# check for secure cookie support
|
||||
if os.environ.get('SECURE_PROXY_SSL_HEADER'):
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
if os.environ.get("SECURE_PROXY_SSL_HEADER"):
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
@ -44,130 +44,168 @@ if os.environ.get('SECURE_PROXY_SSL_HEADER'):
|
||||
##########################################
|
||||
|
||||
# redirect to login page instead of the default homepage when users visits "/"?
|
||||
HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(settings,
|
||||
'HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT',
|
||||
False)
|
||||
HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(
|
||||
settings, "HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT", False
|
||||
)
|
||||
|
||||
HELPDESK_PUBLIC_VIEW_PROTECTOR = getattr(settings,
|
||||
'HELPDESK_PUBLIC_VIEW_PROTECTOR',
|
||||
lambda _: None)
|
||||
HELPDESK_PUBLIC_VIEW_PROTECTOR = getattr(
|
||||
settings, "HELPDESK_PUBLIC_VIEW_PROTECTOR", lambda _: None
|
||||
)
|
||||
|
||||
HELPDESK_STAFF_VIEW_PROTECTOR = getattr(settings,
|
||||
'HELPDESK_STAFF_VIEW_PROTECTOR',
|
||||
lambda _: None)
|
||||
HELPDESK_STAFF_VIEW_PROTECTOR = getattr(
|
||||
settings, "HELPDESK_STAFF_VIEW_PROTECTOR", lambda _: None
|
||||
)
|
||||
|
||||
# Enable ticket and Email attachments
|
||||
#
|
||||
# Caution! Set this to False, unless you have secured access to
|
||||
# the uploaded files. Otherwise anyone on the Internet will be
|
||||
# able to download your ticket attachments.
|
||||
HELPDESK_ENABLE_ATTACHMENTS = getattr(settings,
|
||||
'HELPDESK_ENABLE_ATTACHMENTS',
|
||||
True)
|
||||
HELPDESK_ENABLE_ATTACHMENTS = getattr(settings, "HELPDESK_ENABLE_ATTACHMENTS", True)
|
||||
|
||||
# Enable the Dependencies field on ticket view
|
||||
HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(settings,
|
||||
'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET',
|
||||
True)
|
||||
HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(
|
||||
settings, "HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET", True
|
||||
)
|
||||
|
||||
# Enable the Time spent on field on ticket view
|
||||
HELPDESK_ENABLE_TIME_SPENT_ON_TICKET = getattr(settings,
|
||||
'HELPDESK_ENABLE_TIME_SPENT_ON_TICKET',
|
||||
True)
|
||||
HELPDESK_ENABLE_TIME_SPENT_ON_TICKET = getattr(
|
||||
settings, "HELPDESK_ENABLE_TIME_SPENT_ON_TICKET", True
|
||||
)
|
||||
|
||||
# raises a 404 to anon users. It's like it was invisible
|
||||
HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings,
|
||||
'HELPDESK_ANON_ACCESS_RAISES_404',
|
||||
False)
|
||||
HELPDESK_ANON_ACCESS_RAISES_404 = getattr(
|
||||
settings, "HELPDESK_ANON_ACCESS_RAISES_404", False
|
||||
)
|
||||
|
||||
# Disable Timeline on ticket list
|
||||
HELPDESK_TICKETS_TIMELINE_ENABLED = getattr(
|
||||
settings, 'HELPDESK_TICKETS_TIMELINE_ENABLED', True)
|
||||
settings, "HELPDESK_TICKETS_TIMELINE_ENABLED", True
|
||||
)
|
||||
|
||||
# show extended navigation by default, to all users, irrespective of staff
|
||||
# status?
|
||||
HELPDESK_NAVIGATION_ENABLED = getattr(
|
||||
settings, 'HELPDESK_NAVIGATION_ENABLED', False)
|
||||
HELPDESK_NAVIGATION_ENABLED = getattr(settings, "HELPDESK_NAVIGATION_ENABLED", False)
|
||||
|
||||
# use public CDNs to serve jquery and other javascript by default?
|
||||
# otherwise, use built-in static copy
|
||||
HELPDESK_USE_CDN = getattr(settings, 'HELPDESK_USE_CDN', False)
|
||||
HELPDESK_USE_CDN = getattr(settings, "HELPDESK_USE_CDN", False)
|
||||
|
||||
# show dropdown list of languages that ticket comments can be translated into?
|
||||
HELPDESK_TRANSLATE_TICKET_COMMENTS = getattr(settings,
|
||||
'HELPDESK_TRANSLATE_TICKET_COMMENTS',
|
||||
False)
|
||||
HELPDESK_TRANSLATE_TICKET_COMMENTS = getattr(
|
||||
settings, "HELPDESK_TRANSLATE_TICKET_COMMENTS", False
|
||||
)
|
||||
|
||||
# list of languages to offer. if set to false,
|
||||
# all default google translate languages will be shown.
|
||||
HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG = getattr(settings,
|
||||
'HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG',
|
||||
["en", "de", "es", "fr", "it", "ru"])
|
||||
HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG = getattr(
|
||||
settings,
|
||||
"HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG",
|
||||
["en", "de", "es", "fr", "it", "ru"],
|
||||
)
|
||||
|
||||
# show link to 'change password' on 'User Settings' page?
|
||||
HELPDESK_SHOW_CHANGE_PASSWORD = getattr(
|
||||
settings, 'HELPDESK_SHOW_CHANGE_PASSWORD', False)
|
||||
settings, "HELPDESK_SHOW_CHANGE_PASSWORD", False
|
||||
)
|
||||
|
||||
# allow user to override default layout for 'followups' - work in progress.
|
||||
HELPDESK_FOLLOWUP_MOD = getattr(settings, 'HELPDESK_FOLLOWUP_MOD', False)
|
||||
HELPDESK_FOLLOWUP_MOD = getattr(settings, "HELPDESK_FOLLOWUP_MOD", False)
|
||||
|
||||
# auto-subscribe user to ticket if (s)he responds to a ticket?
|
||||
HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE = getattr(settings,
|
||||
'HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE',
|
||||
False)
|
||||
HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE = getattr(
|
||||
settings, "HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE", False
|
||||
)
|
||||
|
||||
# URL schemes that are allowed within links
|
||||
ALLOWED_URL_SCHEMES = getattr(settings, 'ALLOWED_URL_SCHEMES', (
|
||||
'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp',
|
||||
))
|
||||
ALLOWED_URL_SCHEMES = getattr(
|
||||
settings,
|
||||
"ALLOWED_URL_SCHEMES",
|
||||
(
|
||||
"file",
|
||||
"ftp",
|
||||
"ftps",
|
||||
"http",
|
||||
"https",
|
||||
"irc",
|
||||
"mailto",
|
||||
"sftp",
|
||||
"ssh",
|
||||
"tel",
|
||||
"telnet",
|
||||
"tftp",
|
||||
"vnc",
|
||||
"xmpp",
|
||||
),
|
||||
)
|
||||
|
||||
# Ticket status choices
|
||||
OPEN_STATUS = getattr(settings, 'HELPDESK_TICKET_OPEN_STATUS', 1)
|
||||
REOPENED_STATUS = getattr(settings, 'HELPDESK_TICKET_REOPENED_STATUS', 2)
|
||||
RESOLVED_STATUS = getattr(settings, 'HELPDESK_TICKET_RESOLVED_STATUS', 3)
|
||||
CLOSED_STATUS = getattr(settings, 'HELPDESK_TICKET_CLOSED_STATUS', 4)
|
||||
DUPLICATE_STATUS = getattr(settings, 'HELPDESK_TICKET_DUPLICATE_STATUS', 5)
|
||||
OPEN_STATUS = getattr(settings, "HELPDESK_TICKET_OPEN_STATUS", 1)
|
||||
REOPENED_STATUS = getattr(settings, "HELPDESK_TICKET_REOPENED_STATUS", 2)
|
||||
RESOLVED_STATUS = getattr(settings, "HELPDESK_TICKET_RESOLVED_STATUS", 3)
|
||||
CLOSED_STATUS = getattr(settings, "HELPDESK_TICKET_CLOSED_STATUS", 4)
|
||||
DUPLICATE_STATUS = getattr(settings, "HELPDESK_TICKET_DUPLICATE_STATUS", 5)
|
||||
|
||||
DEFAULT_TICKET_STATUS_CHOICES = (
|
||||
(OPEN_STATUS, _('Open')),
|
||||
(REOPENED_STATUS, _('Reopened')),
|
||||
(RESOLVED_STATUS, _('Resolved')),
|
||||
(CLOSED_STATUS, _('Closed')),
|
||||
(DUPLICATE_STATUS, _('Duplicate')),
|
||||
(OPEN_STATUS, _("Open")),
|
||||
(REOPENED_STATUS, _("Reopened")),
|
||||
(RESOLVED_STATUS, _("Resolved")),
|
||||
(CLOSED_STATUS, _("Closed")),
|
||||
(DUPLICATE_STATUS, _("Duplicate")),
|
||||
)
|
||||
TICKET_STATUS_CHOICES = getattr(
|
||||
settings, "HELPDESK_TICKET_STATUS_CHOICES", DEFAULT_TICKET_STATUS_CHOICES
|
||||
)
|
||||
TICKET_STATUS_CHOICES = getattr(settings,
|
||||
'HELPDESK_TICKET_STATUS_CHOICES',
|
||||
DEFAULT_TICKET_STATUS_CHOICES)
|
||||
|
||||
# List of status choices considered as "open"
|
||||
DEFAULT_TICKET_OPEN_STATUSES = (OPEN_STATUS, REOPENED_STATUS)
|
||||
TICKET_OPEN_STATUSES = getattr(settings,
|
||||
'HELPDESK_TICKET_OPEN_STATUSES',
|
||||
DEFAULT_TICKET_OPEN_STATUSES)
|
||||
DEFAULT_TICKET_OPEN_STATUSES = (OPEN_STATUS, REOPENED_STATUS)
|
||||
TICKET_OPEN_STATUSES = getattr(
|
||||
settings, "HELPDESK_TICKET_OPEN_STATUSES", DEFAULT_TICKET_OPEN_STATUSES
|
||||
)
|
||||
|
||||
# New status list choices depending on current ticket status
|
||||
DEFAULT_TICKET_STATUS_CHOICES_FLOW = {
|
||||
OPEN_STATUS: (OPEN_STATUS, RESOLVED_STATUS, CLOSED_STATUS, DUPLICATE_STATUS,),
|
||||
REOPENED_STATUS: (REOPENED_STATUS, RESOLVED_STATUS, CLOSED_STATUS, DUPLICATE_STATUS,),
|
||||
RESOLVED_STATUS: (REOPENED_STATUS, RESOLVED_STATUS, CLOSED_STATUS,),
|
||||
CLOSED_STATUS: (REOPENED_STATUS, CLOSED_STATUS,),
|
||||
DUPLICATE_STATUS: (REOPENED_STATUS, DUPLICATE_STATUS,),
|
||||
OPEN_STATUS: (
|
||||
OPEN_STATUS,
|
||||
RESOLVED_STATUS,
|
||||
CLOSED_STATUS,
|
||||
DUPLICATE_STATUS,
|
||||
),
|
||||
REOPENED_STATUS: (
|
||||
REOPENED_STATUS,
|
||||
RESOLVED_STATUS,
|
||||
CLOSED_STATUS,
|
||||
DUPLICATE_STATUS,
|
||||
),
|
||||
RESOLVED_STATUS: (
|
||||
REOPENED_STATUS,
|
||||
RESOLVED_STATUS,
|
||||
CLOSED_STATUS,
|
||||
),
|
||||
CLOSED_STATUS: (
|
||||
REOPENED_STATUS,
|
||||
CLOSED_STATUS,
|
||||
),
|
||||
DUPLICATE_STATUS: (
|
||||
REOPENED_STATUS,
|
||||
DUPLICATE_STATUS,
|
||||
),
|
||||
}
|
||||
TICKET_STATUS_CHOICES_FLOW = getattr(settings,
|
||||
'HELPDESK_TICKET_STATUS_CHOICES_FLOW',
|
||||
DEFAULT_TICKET_STATUS_CHOICES_FLOW)
|
||||
TICKET_STATUS_CHOICES_FLOW = getattr(
|
||||
settings, "HELPDESK_TICKET_STATUS_CHOICES_FLOW", DEFAULT_TICKET_STATUS_CHOICES_FLOW
|
||||
)
|
||||
|
||||
# Ticket priority choices
|
||||
DEFAULT_TICKET_PRIORITY_CHOICES = (
|
||||
(1, _('1. Critical')),
|
||||
(2, _('2. High')),
|
||||
(3, _('3. Normal')),
|
||||
(4, _('4. Low')),
|
||||
(5, _('5. Very Low')),
|
||||
(1, _("1. Critical")),
|
||||
(2, _("2. High")),
|
||||
(3, _("3. Normal")),
|
||||
(4, _("4. Low")),
|
||||
(5, _("5. Very Low")),
|
||||
)
|
||||
TICKET_PRIORITY_CHOICES = getattr(
|
||||
settings, "HELPDESK_TICKET_PRIORITY_CHOICES", DEFAULT_TICKET_PRIORITY_CHOICES
|
||||
)
|
||||
TICKET_PRIORITY_CHOICES = getattr(settings,
|
||||
'HELPDESK_TICKET_PRIORITY_CHOICES',
|
||||
DEFAULT_TICKET_PRIORITY_CHOICES)
|
||||
|
||||
|
||||
#########################
|
||||
@ -175,59 +213,55 @@ TICKET_PRIORITY_CHOICES = getattr(settings,
|
||||
#########################
|
||||
|
||||
# Follow-ups automatic time_spent calculation
|
||||
FOLLOWUP_TIME_SPENT_AUTO = getattr(settings,
|
||||
'HELPDESK_FOLLOWUP_TIME_SPENT_AUTO',
|
||||
False)
|
||||
FOLLOWUP_TIME_SPENT_AUTO = getattr(settings, "HELPDESK_FOLLOWUP_TIME_SPENT_AUTO", False)
|
||||
|
||||
# Calculate time_spent according to open hours
|
||||
FOLLOWUP_TIME_SPENT_OPENING_HOURS = getattr(settings,
|
||||
'HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS',
|
||||
{})
|
||||
FOLLOWUP_TIME_SPENT_OPENING_HOURS = getattr(
|
||||
settings, "HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS", {}
|
||||
)
|
||||
|
||||
# Holidays don't count for time_spent calculation
|
||||
FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = getattr(settings,
|
||||
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS',
|
||||
())
|
||||
FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = getattr(
|
||||
settings, "HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS", ()
|
||||
)
|
||||
|
||||
# Time doesn't count for listed ticket statuses
|
||||
FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = getattr(settings,
|
||||
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES',
|
||||
())
|
||||
FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = getattr(
|
||||
settings, "HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES", ()
|
||||
)
|
||||
|
||||
# Time doesn't count for listed queues slugs
|
||||
FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = getattr(settings,
|
||||
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES',
|
||||
())
|
||||
FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = getattr(
|
||||
settings, "HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES", ()
|
||||
)
|
||||
|
||||
############################
|
||||
# options for public pages #
|
||||
############################
|
||||
|
||||
# show 'view a ticket' section on public page?
|
||||
HELPDESK_VIEW_A_TICKET_PUBLIC = getattr(
|
||||
settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC', True)
|
||||
HELPDESK_VIEW_A_TICKET_PUBLIC = getattr(settings, "HELPDESK_VIEW_A_TICKET_PUBLIC", True)
|
||||
|
||||
# show 'submit a ticket' section on public page?
|
||||
HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(
|
||||
settings, 'HELPDESK_SUBMIT_A_TICKET_PUBLIC', True)
|
||||
settings, "HELPDESK_SUBMIT_A_TICKET_PUBLIC", True
|
||||
)
|
||||
|
||||
# change that to custom class to have extra fields or validation (like captcha)
|
||||
HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr(
|
||||
settings,
|
||||
"HELPDESK_PUBLIC_TICKET_FORM_CLASS",
|
||||
"helpdesk.forms.PublicTicketForm"
|
||||
settings, "HELPDESK_PUBLIC_TICKET_FORM_CLASS", "helpdesk.forms.PublicTicketForm"
|
||||
)
|
||||
|
||||
# Custom fields constants
|
||||
CUSTOMFIELD_TO_FIELD_DICT = {
|
||||
'boolean': forms.BooleanField,
|
||||
'date': forms.DateField,
|
||||
'time': forms.TimeField,
|
||||
'datetime': forms.DateTimeField,
|
||||
'email': forms.EmailField,
|
||||
'url': forms.URLField,
|
||||
'ipaddress': forms.GenericIPAddressField,
|
||||
'slug': forms.SlugField,
|
||||
"boolean": forms.BooleanField,
|
||||
"date": forms.DateField,
|
||||
"time": forms.TimeField,
|
||||
"datetime": forms.DateTimeField,
|
||||
"email": forms.EmailField,
|
||||
"url": forms.URLField,
|
||||
"ipaddress": forms.GenericIPAddressField,
|
||||
"slug": forms.SlugField,
|
||||
}
|
||||
CUSTOMFIELD_DATE_FORMAT = "%Y-%m-%d"
|
||||
CUSTOMFIELD_TIME_FORMAT = "%H:%M:%S"
|
||||
@ -238,48 +272,58 @@ CUSTOMFIELD_DATETIME_FORMAT = f"{CUSTOMFIELD_DATE_FORMAT}T%H:%M"
|
||||
# options for update_ticket views #
|
||||
###################################
|
||||
|
||||
''' options for update_ticket views '''
|
||||
""" options for update_ticket views """
|
||||
# allow non-staff users to interact with tickets?
|
||||
# can be True/False or a callable accepting the active user and returning
|
||||
# True if they must be considered helpdesk staff
|
||||
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(
|
||||
settings, 'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE', False)
|
||||
if not (HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE in (True, False) or callable(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE)):
|
||||
settings, "HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE", False
|
||||
)
|
||||
if not (
|
||||
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE in (True, False)
|
||||
or callable(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE)
|
||||
):
|
||||
warnings.warn(
|
||||
"HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE should be set to either True/False or a callable.",
|
||||
RuntimeWarning
|
||||
RuntimeWarning,
|
||||
)
|
||||
|
||||
# show edit buttons in ticket follow ups.
|
||||
HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(settings,
|
||||
'HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP',
|
||||
True)
|
||||
HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(
|
||||
settings, "HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP", True
|
||||
)
|
||||
|
||||
HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST = getattr(settings,
|
||||
'HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST',
|
||||
[])
|
||||
HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST = getattr(
|
||||
settings, "HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST", []
|
||||
)
|
||||
|
||||
# show delete buttons in ticket follow ups if user is 'superuser'
|
||||
HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP = getattr(
|
||||
settings, 'HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP', False)
|
||||
settings, "HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP", False
|
||||
)
|
||||
|
||||
# make all updates public by default? this will hide the 'is this update
|
||||
# public' checkbox
|
||||
HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr(
|
||||
settings, 'HELPDESK_UPDATE_PUBLIC_DEFAULT', False)
|
||||
settings, "HELPDESK_UPDATE_PUBLIC_DEFAULT", False
|
||||
)
|
||||
|
||||
# only show staff users in ticket owner drop-downs
|
||||
HELPDESK_STAFF_ONLY_TICKET_OWNERS = getattr(
|
||||
settings, 'HELPDESK_STAFF_ONLY_TICKET_OWNERS', False)
|
||||
settings, "HELPDESK_STAFF_ONLY_TICKET_OWNERS", False
|
||||
)
|
||||
|
||||
# only show staff users in ticket cc drop-down
|
||||
HELPDESK_STAFF_ONLY_TICKET_CC = getattr(
|
||||
settings, 'HELPDESK_STAFF_ONLY_TICKET_CC', False)
|
||||
settings, "HELPDESK_STAFF_ONLY_TICKET_CC", False
|
||||
)
|
||||
|
||||
# allow the subject to have a configurable template.
|
||||
HELPDESK_EMAIL_SUBJECT_TEMPLATE = getattr(
|
||||
settings, 'HELPDESK_EMAIL_SUBJECT_TEMPLATE',
|
||||
"{{ ticket.ticket }} {{ ticket.title|safe }} %(subject)s")
|
||||
settings,
|
||||
"HELPDESK_EMAIL_SUBJECT_TEMPLATE",
|
||||
"{{ ticket.ticket }} {{ ticket.title|safe }} %(subject)s",
|
||||
)
|
||||
# since django-helpdesk may not work correctly without the ticket ID
|
||||
# in the subject, let's do a check for it quick:
|
||||
if HELPDESK_EMAIL_SUBJECT_TEMPLATE.find("ticket.ticket") < 0:
|
||||
@ -287,12 +331,14 @@ if HELPDESK_EMAIL_SUBJECT_TEMPLATE.find("ticket.ticket") < 0:
|
||||
|
||||
# default fallback locale when queue locale not found
|
||||
HELPDESK_EMAIL_FALLBACK_LOCALE = getattr(
|
||||
settings, 'HELPDESK_EMAIL_FALLBACK_LOCALE', 'en')
|
||||
settings, "HELPDESK_EMAIL_FALLBACK_LOCALE", "en"
|
||||
)
|
||||
|
||||
# default maximum email attachment size, in bytes
|
||||
# only attachments smaller than this size will be sent via email
|
||||
HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr(
|
||||
settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000)
|
||||
settings, "HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE", 512000
|
||||
)
|
||||
|
||||
|
||||
########################################
|
||||
@ -301,7 +347,8 @@ HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr(
|
||||
|
||||
# hide the 'assigned to' / 'Case owner' field from the 'create_ticket' view?
|
||||
HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr(
|
||||
settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False)
|
||||
settings, "HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO", False
|
||||
)
|
||||
|
||||
|
||||
#################
|
||||
@ -309,33 +356,37 @@ HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr(
|
||||
#################
|
||||
|
||||
# default Queue email submission settings
|
||||
QUEUE_EMAIL_BOX_TYPE = getattr(settings, 'QUEUE_EMAIL_BOX_TYPE', None)
|
||||
QUEUE_EMAIL_BOX_SSL = getattr(settings, 'QUEUE_EMAIL_BOX_SSL', None)
|
||||
QUEUE_EMAIL_BOX_HOST = getattr(settings, 'QUEUE_EMAIL_BOX_HOST', None)
|
||||
QUEUE_EMAIL_BOX_USER = getattr(settings, 'QUEUE_EMAIL_BOX_USER', None)
|
||||
QUEUE_EMAIL_BOX_PASSWORD = getattr(settings, 'QUEUE_EMAIL_BOX_PASSWORD', None)
|
||||
QUEUE_EMAIL_BOX_TYPE = getattr(settings, "QUEUE_EMAIL_BOX_TYPE", None)
|
||||
QUEUE_EMAIL_BOX_SSL = getattr(settings, "QUEUE_EMAIL_BOX_SSL", None)
|
||||
QUEUE_EMAIL_BOX_HOST = getattr(settings, "QUEUE_EMAIL_BOX_HOST", None)
|
||||
QUEUE_EMAIL_BOX_USER = getattr(settings, "QUEUE_EMAIL_BOX_USER", None)
|
||||
QUEUE_EMAIL_BOX_PASSWORD = getattr(settings, "QUEUE_EMAIL_BOX_PASSWORD", None)
|
||||
|
||||
# only process emails with a valid tracking ID? (throws away all other mail)
|
||||
QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr(
|
||||
settings, 'QUEUE_EMAIL_BOX_UPDATE_ONLY', False)
|
||||
QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr(settings, "QUEUE_EMAIL_BOX_UPDATE_ONLY", False)
|
||||
|
||||
# only allow users to access queues that they are members of?
|
||||
HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = getattr(
|
||||
settings, 'HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION', False)
|
||||
settings, "HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION", False
|
||||
)
|
||||
|
||||
# use https in the email links
|
||||
HELPDESK_USE_HTTPS_IN_EMAIL_LINK = getattr(
|
||||
settings, 'HELPDESK_USE_HTTPS_IN_EMAIL_LINK', settings.SECURE_SSL_REDIRECT)
|
||||
settings, "HELPDESK_USE_HTTPS_IN_EMAIL_LINK", settings.SECURE_SSL_REDIRECT
|
||||
)
|
||||
|
||||
# Default to True for backwards compatibility
|
||||
HELPDESK_TEAMS_MODE_ENABLED = getattr(settings, 'HELPDESK_TEAMS_MODE_ENABLED', True)
|
||||
HELPDESK_TEAMS_MODE_ENABLED = getattr(settings, "HELPDESK_TEAMS_MODE_ENABLED", True)
|
||||
if HELPDESK_TEAMS_MODE_ENABLED:
|
||||
HELPDESK_TEAMS_MODEL = getattr(
|
||||
settings, 'HELPDESK_TEAMS_MODEL', 'pinax_teams.Team')
|
||||
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = getattr(settings, 'HELPDESK_TEAMS_MIGRATION_DEPENDENCIES', [
|
||||
('pinax_teams', '0004_auto_20170511_0856')])
|
||||
HELPDESK_TEAMS_MODEL = getattr(settings, "HELPDESK_TEAMS_MODEL", "pinax_teams.Team")
|
||||
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = getattr(
|
||||
settings,
|
||||
"HELPDESK_TEAMS_MIGRATION_DEPENDENCIES",
|
||||
[("pinax_teams", "0004_auto_20170511_0856")],
|
||||
)
|
||||
HELPDESK_KBITEM_TEAM_GETTER = getattr(
|
||||
settings, 'HELPDESK_KBITEM_TEAM_GETTER', lambda kbitem: kbitem.team)
|
||||
settings, "HELPDESK_KBITEM_TEAM_GETTER", lambda kbitem: kbitem.team
|
||||
)
|
||||
else:
|
||||
HELPDESK_TEAMS_MODEL = settings.AUTH_USER_MODEL
|
||||
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = []
|
||||
@ -343,35 +394,38 @@ else:
|
||||
|
||||
# show knowledgebase links?
|
||||
# If Teams mode is enabled then it has to be on
|
||||
HELPDESK_KB_ENABLED = True if HELPDESK_TEAMS_MODE_ENABLED else getattr(settings, 'HELPDESK_KB_ENABLED', True)
|
||||
HELPDESK_KB_ENABLED = (
|
||||
True
|
||||
if HELPDESK_TEAMS_MODE_ENABLED
|
||||
else getattr(settings, "HELPDESK_KB_ENABLED", True)
|
||||
)
|
||||
|
||||
# Include all signatures and forwards in the first ticket message if set
|
||||
# Useful if you get forwards dropped from them while they are useful part
|
||||
# of request
|
||||
HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr(
|
||||
settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False)
|
||||
settings, "HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL", False
|
||||
)
|
||||
|
||||
# If set then we always save incoming emails as .eml attachments
|
||||
# which is quite noisy but very helpful for complicated markup, forwards and so on
|
||||
# (which gets stripped/corrupted otherwise)
|
||||
HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr(
|
||||
settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False)
|
||||
settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False
|
||||
)
|
||||
|
||||
#######################
|
||||
# email OAUTH #
|
||||
#######################
|
||||
|
||||
HELPDESK_OAUTH = getattr(
|
||||
settings, 'HELPDESK_OAUTH', {
|
||||
"token_url": "",
|
||||
"client_id": "",
|
||||
"secret": "",
|
||||
"scope": [""]
|
||||
}
|
||||
settings,
|
||||
"HELPDESK_OAUTH",
|
||||
{"token_url": "", "client_id": "", "secret": "", "scope": [""]},
|
||||
)
|
||||
|
||||
# Set Debug Logging Level for IMAP Services. Default to '0' for No Debugging
|
||||
HELPDESK_IMAP_DEBUG_LEVEL = getattr(settings, 'HELPDESK_IMAP_DEBUG_LEVEL', 0)
|
||||
HELPDESK_IMAP_DEBUG_LEVEL = getattr(settings, "HELPDESK_IMAP_DEBUG_LEVEL", 0)
|
||||
|
||||
#############################################
|
||||
# file permissions - Attachment directories #
|
||||
@ -379,29 +433,60 @@ HELPDESK_IMAP_DEBUG_LEVEL = getattr(settings, 'HELPDESK_IMAP_DEBUG_LEVEL', 0)
|
||||
|
||||
# Attachment directories should be created with permission 755 (rwxr-xr-x)
|
||||
# Override it in your own Django settings.py
|
||||
HELPDESK_ATTACHMENT_DIR_PERMS = int(getattr(settings, 'HELPDESK_ATTACHMENT_DIR_PERMS', "755"), 8)
|
||||
HELPDESK_ATTACHMENT_DIR_PERMS = int(
|
||||
getattr(settings, "HELPDESK_ATTACHMENT_DIR_PERMS", "755"), 8
|
||||
)
|
||||
|
||||
HELPDESK_VALID_EXTENSIONS = getattr(settings, 'VALID_EXTENSIONS', None)
|
||||
HELPDESK_VALID_EXTENSIONS = getattr(settings, "VALID_EXTENSIONS", None)
|
||||
if HELPDESK_VALID_EXTENSIONS:
|
||||
# Print to stderr
|
||||
print("VALID_EXTENSIONS is deprecated, use HELPDESK_VALID_EXTENSIONS instead", file=sys.stderr)
|
||||
print(
|
||||
"VALID_EXTENSIONS is deprecated, use HELPDESK_VALID_EXTENSIONS instead",
|
||||
file=sys.stderr,
|
||||
)
|
||||
else:
|
||||
HELPDESK_VALID_EXTENSIONS = getattr(settings, 'HELPDESK_VALID_EXTENSIONS', ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml'])
|
||||
HELPDESK_VALID_EXTENSIONS = getattr(
|
||||
settings,
|
||||
"HELPDESK_VALID_EXTENSIONS",
|
||||
[
|
||||
".txt",
|
||||
".asc",
|
||||
".htm",
|
||||
".html",
|
||||
".pdf",
|
||||
".doc",
|
||||
".docx",
|
||||
".odt",
|
||||
".jpg",
|
||||
".png",
|
||||
".eml",
|
||||
],
|
||||
)
|
||||
|
||||
HELPDESK_VALIDATE_ATTACHMENT_TYPES = getattr(
|
||||
settings, "HELPDESK_VALIDATE_ATTACHMENT_TYPES", True
|
||||
)
|
||||
|
||||
HELPDESK_VALIDATE_ATTACHMENT_TYPES = getattr(settings, 'HELPDESK_VALIDATE_ATTACHMENT_TYPES', True)
|
||||
|
||||
def get_followup_webhook_urls():
|
||||
urls = os.environ.get('HELPDESK_FOLLOWUP_WEBHOOK_URLS', None)
|
||||
urls = os.environ.get("HELPDESK_FOLLOWUP_WEBHOOK_URLS", None)
|
||||
if urls:
|
||||
return re.split(r'[\s],[\s]', urls)
|
||||
return re.split(r"[\s],[\s]", urls)
|
||||
|
||||
|
||||
HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS = getattr(
|
||||
settings, "HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS", get_followup_webhook_urls
|
||||
)
|
||||
|
||||
HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS = getattr(settings, 'HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS', get_followup_webhook_urls)
|
||||
|
||||
def get_new_ticket_webhook_urls():
|
||||
urls = os.environ.get('HELPDESK_NEW_TICKET_WEBHOOK_URLS', None)
|
||||
urls = os.environ.get("HELPDESK_NEW_TICKET_WEBHOOK_URLS", None)
|
||||
if urls:
|
||||
return urls.split(',')
|
||||
return urls.split(",")
|
||||
|
||||
HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS = getattr(settings, 'HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS', get_new_ticket_webhook_urls)
|
||||
|
||||
HELPDESK_WEBHOOK_TIMEOUT = getattr(settings, 'HELPDESK_WEBHOOK_TIMEOUT', 3)
|
||||
HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS = getattr(
|
||||
settings, "HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS", get_new_ticket_webhook_urls
|
||||
)
|
||||
|
||||
HELPDESK_WEBHOOK_TIMEOUT = getattr(settings, "HELPDESK_WEBHOOK_TIMEOUT", 3)
|
||||
|
@ -4,4 +4,4 @@ import django.dispatch
|
||||
new_ticket_done = django.dispatch.Signal()
|
||||
|
||||
# create a signal for ticket_update view
|
||||
update_ticket_done = django.dispatch.Signal()
|
||||
update_ticket_done = django.dispatch.Signal()
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.safestring import mark_safe
|
||||
import logging
|
||||
@ -6,17 +5,19 @@ import os
|
||||
from smtplib import SMTPException
|
||||
|
||||
|
||||
logger = logging.getLogger('helpdesk')
|
||||
logger = logging.getLogger("helpdesk")
|
||||
|
||||
|
||||
def send_templated_mail(template_name,
|
||||
context,
|
||||
recipients,
|
||||
sender=None,
|
||||
bcc=None,
|
||||
fail_silently=False,
|
||||
files=None,
|
||||
extra_headers=None):
|
||||
def send_templated_mail(
|
||||
template_name,
|
||||
context,
|
||||
recipients,
|
||||
sender=None,
|
||||
bcc=None,
|
||||
fail_silently=False,
|
||||
files=None,
|
||||
extra_headers=None,
|
||||
):
|
||||
"""
|
||||
send_templated_mail() is a wrapper around Django's e-mail routines that
|
||||
allows us to easily send multipart (text/plain & text/html) e-mails using
|
||||
@ -48,77 +49,87 @@ def send_templated_mail(template_name,
|
||||
"""
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template import engines
|
||||
from_string = engines['django'].from_string
|
||||
|
||||
from_string = engines["django"].from_string
|
||||
|
||||
from helpdesk.models import EmailTemplate
|
||||
from helpdesk.settings import HELPDESK_EMAIL_FALLBACK_LOCALE, HELPDESK_EMAIL_SUBJECT_TEMPLATE
|
||||
from helpdesk.settings import (
|
||||
HELPDESK_EMAIL_FALLBACK_LOCALE,
|
||||
HELPDESK_EMAIL_SUBJECT_TEMPLATE,
|
||||
)
|
||||
|
||||
headers = extra_headers or {}
|
||||
|
||||
locale = context['queue'].get('locale') or HELPDESK_EMAIL_FALLBACK_LOCALE
|
||||
locale = context["queue"].get("locale") or HELPDESK_EMAIL_FALLBACK_LOCALE
|
||||
|
||||
try:
|
||||
t = EmailTemplate.objects.get(
|
||||
template_name__iexact=template_name, locale=locale)
|
||||
template_name__iexact=template_name, locale=locale
|
||||
)
|
||||
except EmailTemplate.DoesNotExist:
|
||||
try:
|
||||
t = EmailTemplate.objects.get(
|
||||
template_name__iexact=template_name, locale__isnull=True)
|
||||
template_name__iexact=template_name, locale__isnull=True
|
||||
)
|
||||
except EmailTemplate.DoesNotExist:
|
||||
logger.warning(
|
||||
'template "%s" does not exist, no mail sent', template_name)
|
||||
logger.warning('template "%s" does not exist, no mail sent', template_name)
|
||||
return # just ignore if template doesn't exist
|
||||
|
||||
subject_part = from_string(
|
||||
HELPDESK_EMAIL_SUBJECT_TEMPLATE % {
|
||||
"subject": t.subject
|
||||
}).render(context).replace('\n', '').replace('\r', '')
|
||||
subject_part = (
|
||||
from_string(HELPDESK_EMAIL_SUBJECT_TEMPLATE % {"subject": t.subject})
|
||||
.render(context)
|
||||
.replace("\n", "")
|
||||
.replace("\r", "")
|
||||
)
|
||||
|
||||
footer_file = os.path.join('helpdesk', locale, 'email_text_footer.txt')
|
||||
footer_file = os.path.join("helpdesk", locale, "email_text_footer.txt")
|
||||
|
||||
text_part = from_string(
|
||||
"%s\n\n{%% include '%s' %%}" % (t.plain_text, footer_file)
|
||||
).render(context)
|
||||
|
||||
email_html_base_file = os.path.join(
|
||||
'helpdesk', locale, 'email_html_base.html')
|
||||
email_html_base_file = os.path.join("helpdesk", locale, "email_html_base.html")
|
||||
# keep new lines in html emails
|
||||
if 'comment' in context:
|
||||
context['comment'] = mark_safe(
|
||||
context['comment'].replace('\r\n', '<br>'))
|
||||
if "comment" in context:
|
||||
context["comment"] = mark_safe(context["comment"].replace("\r\n", "<br>"))
|
||||
|
||||
html_part = from_string(
|
||||
"{%% extends '%s' %%}"
|
||||
"{%% block title %%}%s{%% endblock %%}"
|
||||
"{%% block content %%}%s{%% endblock %%}" %
|
||||
(email_html_base_file, t.heading, t.html)
|
||||
"{%% block content %%}%s{%% endblock %%}"
|
||||
% (email_html_base_file, t.heading, t.html)
|
||||
).render(context)
|
||||
|
||||
if isinstance(recipients, str):
|
||||
if recipients.find(','):
|
||||
recipients = recipients.split(',')
|
||||
if recipients.find(","):
|
||||
recipients = recipients.split(",")
|
||||
elif type(recipients) is not list:
|
||||
recipients = [recipients]
|
||||
|
||||
msg = EmailMultiAlternatives(subject_part, text_part,
|
||||
sender or settings.DEFAULT_FROM_EMAIL,
|
||||
recipients, bcc=bcc,
|
||||
headers=headers)
|
||||
msg = EmailMultiAlternatives(
|
||||
subject_part,
|
||||
text_part,
|
||||
sender or settings.DEFAULT_FROM_EMAIL,
|
||||
recipients,
|
||||
bcc=bcc,
|
||||
headers=headers,
|
||||
)
|
||||
msg.attach_alternative(html_part, "text/html")
|
||||
|
||||
if files:
|
||||
for filename, filefield in files:
|
||||
filefield.open('rb')
|
||||
filefield.open("rb")
|
||||
content = filefield.read()
|
||||
msg.attach(filename, content)
|
||||
filefield.close()
|
||||
logger.debug('Sending email to: {!r}'.format(recipients))
|
||||
logger.debug("Sending email to: {!r}".format(recipients))
|
||||
|
||||
try:
|
||||
return msg.send()
|
||||
except SMTPException as e:
|
||||
logger.exception(
|
||||
'SMTPException raised while sending email to {}'.format(recipients))
|
||||
"SMTPException raised while sending email to {}".format(recipients)
|
||||
)
|
||||
if not fail_silently:
|
||||
raise e
|
||||
return 0
|
||||
|
@ -14,10 +14,9 @@ logger = logging.getLogger(__name__)
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.filter(name='is_helpdesk_staff')
|
||||
@register.filter(name="is_helpdesk_staff")
|
||||
def helpdesk_staff(user):
|
||||
try:
|
||||
return is_helpdesk_staff(user)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"'helpdesk_staff' template tag (django-helpdesk) crashed")
|
||||
logger.exception("'helpdesk_staff' template tag (django-helpdesk) crashed")
|
||||
|
@ -2,7 +2,11 @@ from datetime import datetime
|
||||
from django.conf import settings
|
||||
from django.template import Library
|
||||
from django.template.defaultfilters import date as date_filter
|
||||
from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT
|
||||
from helpdesk.forms import (
|
||||
CUSTOMFIELD_DATE_FORMAT,
|
||||
CUSTOMFIELD_DATETIME_FORMAT,
|
||||
CUSTOMFIELD_TIME_FORMAT,
|
||||
)
|
||||
|
||||
|
||||
register = Library()
|
||||
@ -10,7 +14,7 @@ register = Library()
|
||||
|
||||
@register.filter
|
||||
def get(value, arg, default=None):
|
||||
""" Call the dictionary get function """
|
||||
"""Call the dictionary get function"""
|
||||
return value.get(arg, default)
|
||||
|
||||
|
||||
@ -21,16 +25,21 @@ def datetime_string_format(value):
|
||||
:return: String - reformatted to default datetime, date, or time string if received in one of the expected formats
|
||||
"""
|
||||
try:
|
||||
new_value = date_filter(datetime.strptime(
|
||||
value, CUSTOMFIELD_DATETIME_FORMAT), settings.DATETIME_FORMAT)
|
||||
new_value = date_filter(
|
||||
datetime.strptime(value, CUSTOMFIELD_DATETIME_FORMAT),
|
||||
settings.DATETIME_FORMAT,
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
try:
|
||||
new_value = date_filter(datetime.strptime(
|
||||
value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT)
|
||||
new_value = date_filter(
|
||||
datetime.strptime(value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
try:
|
||||
new_value = date_filter(datetime.strptime(
|
||||
value, CUSTOMFIELD_TIME_FORMAT), settings.TIME_FORMAT)
|
||||
new_value = date_filter(
|
||||
datetime.strptime(value, CUSTOMFIELD_TIME_FORMAT),
|
||||
settings.TIME_FORMAT,
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
# If NoneType return empty string, else return original value
|
||||
new_value = "" if value is None else value
|
||||
|
@ -4,6 +4,7 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
templatetags/load_helpdesk_settings.py - returns the settings as defined in
|
||||
django-helpdesk/helpdesk/settings.py
|
||||
"""
|
||||
|
||||
from django.template import Library
|
||||
from helpdesk import settings as helpdesk_settings_config
|
||||
|
||||
@ -13,11 +14,14 @@ def load_helpdesk_settings(request):
|
||||
return helpdesk_settings_config
|
||||
except Exception as e:
|
||||
import sys
|
||||
print("'load_helpdesk_settings' template tag (django-helpdesk) crashed with following error:",
|
||||
file=sys.stderr)
|
||||
|
||||
print(
|
||||
"'load_helpdesk_settings' template tag (django-helpdesk) crashed with following error:",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(e, file=sys.stderr)
|
||||
return ''
|
||||
return ""
|
||||
|
||||
|
||||
register = Library()
|
||||
register.filter('load_helpdesk_settings', load_helpdesk_settings)
|
||||
register.filter("load_helpdesk_settings", load_helpdesk_settings)
|
||||
|
@ -5,6 +5,7 @@ templatetags/saved_queries.py - This template tag returns previously saved
|
||||
queries. Therefore you don't need to modify
|
||||
any views.
|
||||
"""
|
||||
|
||||
from django import template
|
||||
from django.db.models import Q
|
||||
from helpdesk.models import SavedSearch
|
||||
@ -23,7 +24,10 @@ def saved_queries(user):
|
||||
return user_saved_queries
|
||||
except Exception as e:
|
||||
import sys
|
||||
print("'saved_queries' template tag (django-helpdesk) crashed with following error:",
|
||||
file=sys.stderr)
|
||||
|
||||
print(
|
||||
"'saved_queries' template tag (django-helpdesk) crashed with following error:",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(e, file=sys.stderr)
|
||||
return ''
|
||||
return ""
|
||||
|
@ -19,7 +19,7 @@ import re
|
||||
|
||||
|
||||
def num_to_link(text):
|
||||
if text == '':
|
||||
if text == "":
|
||||
return text
|
||||
|
||||
matches = []
|
||||
@ -28,7 +28,7 @@ def num_to_link(text):
|
||||
|
||||
for match in reversed(matches):
|
||||
number = match.groups()[0]
|
||||
url = reverse('helpdesk:view', args=[number])
|
||||
url = reverse("helpdesk:view", args=[number])
|
||||
try:
|
||||
ticket = Ticket.objects.get(id=number)
|
||||
except Ticket.DoesNotExist:
|
||||
@ -36,8 +36,16 @@ def num_to_link(text):
|
||||
|
||||
if ticket:
|
||||
style = ticket.get_status_display()
|
||||
text = "%s <a href='%s' class='ticket_link_status ticket_link_status_%s'>#%s</a>%s" % (
|
||||
text[:match.start() + 1], url, style, match.groups()[0], text[match.end():])
|
||||
text = (
|
||||
"%s <a href='%s' class='ticket_link_status ticket_link_status_%s'>#%s</a>%s"
|
||||
% (
|
||||
text[: match.start() + 1],
|
||||
url,
|
||||
style,
|
||||
match.groups()[0],
|
||||
text[match.end() :],
|
||||
)
|
||||
)
|
||||
return mark_safe(text)
|
||||
|
||||
|
||||
|
@ -20,9 +20,7 @@ def user_admin_url(action):
|
||||
except AttributeError: # module_name alias removed in django 1.8
|
||||
model_name = user._meta.model_name.lower()
|
||||
|
||||
return 'admin:%s_%s_%s' % (
|
||||
user._meta.app_label, model_name,
|
||||
action)
|
||||
return "admin:%s_%s_%s" % (user._meta.app_label, model_name, action)
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
@ -8,16 +8,15 @@ import sys
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def get_user(username='helpdesk.staff',
|
||||
password='password',
|
||||
is_staff=False,
|
||||
is_superuser=False):
|
||||
def get_user(
|
||||
username="helpdesk.staff", password="password", is_staff=False, is_superuser=False
|
||||
):
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
except User.DoesNotExist:
|
||||
user = User.objects.create_user(username=username,
|
||||
password=password,
|
||||
email='%s@example.com' % username)
|
||||
user = User.objects.create_user(
|
||||
username=username, password=password, email="%s@example.com" % username
|
||||
)
|
||||
user.is_staff = is_staff
|
||||
user.is_superuser = is_superuser
|
||||
user.save()
|
||||
@ -32,7 +31,6 @@ def get_staff_user():
|
||||
|
||||
|
||||
def reload_urlconf(urlconf=None):
|
||||
|
||||
from importlib import reload
|
||||
|
||||
if urlconf is None:
|
||||
@ -47,25 +45,29 @@ def reload_urlconf(urlconf=None):
|
||||
reload(sys.modules[urlconf])
|
||||
|
||||
from django.urls import clear_url_caches
|
||||
|
||||
clear_url_caches()
|
||||
|
||||
|
||||
def create_ticket(**kwargs):
|
||||
q = kwargs.get('queue', None)
|
||||
q = kwargs.get("queue", None)
|
||||
if q is None:
|
||||
try:
|
||||
q = Queue.objects.all()[0]
|
||||
except IndexError:
|
||||
q = Queue.objects.create(title='Test Q', slug='test', )
|
||||
q = Queue.objects.create(
|
||||
title="Test Q",
|
||||
slug="test",
|
||||
)
|
||||
data = {
|
||||
'title': "I wish to register a complaint",
|
||||
'queue': q,
|
||||
"title": "I wish to register a complaint",
|
||||
"queue": q,
|
||||
}
|
||||
data.update(kwargs)
|
||||
return Ticket.objects.create(**data)
|
||||
|
||||
|
||||
HELPDESK_URLCONF = 'helpdesk.urls'
|
||||
HELPDESK_URLCONF = "helpdesk.urls"
|
||||
|
||||
|
||||
def print_response(response, stdout=False):
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import base64
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
@ -13,7 +12,7 @@ from rest_framework.status import (
|
||||
HTTP_201_CREATED,
|
||||
HTTP_204_NO_CONTENT,
|
||||
HTTP_400_BAD_REQUEST,
|
||||
HTTP_403_FORBIDDEN
|
||||
HTTP_403_FORBIDDEN,
|
||||
)
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
@ -24,106 +23,124 @@ class TicketTest(APITestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.queue = Queue.objects.create(
|
||||
title='Test Queue',
|
||||
slug='test-queue',
|
||||
title="Test Queue",
|
||||
slug="test-queue",
|
||||
)
|
||||
|
||||
def test_create_api_ticket_not_authenticated_user(self):
|
||||
response = self.client.post('/api/tickets/')
|
||||
response = self.client.post("/api/tickets/")
|
||||
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_create_api_ticket_authenticated_non_staff_user(self):
|
||||
non_staff_user = User.objects.create_user(username='test')
|
||||
non_staff_user = User.objects.create_user(username="test")
|
||||
self.client.force_authenticate(non_staff_user)
|
||||
response = self.client.post('/api/tickets/')
|
||||
response = self.client.post("/api/tickets/")
|
||||
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_create_api_ticket_no_data(self):
|
||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
||||
staff_user = User.objects.create_user(username="test", is_staff=True)
|
||||
self.client.force_authenticate(staff_user)
|
||||
response = self.client.post('/api/tickets/')
|
||||
response = self.client.post("/api/tickets/")
|
||||
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(response.data, {
|
||||
'queue': [ErrorDetail(string='This field is required.', code='required')],
|
||||
'title': [ErrorDetail(string='This field is required.', code='required')]
|
||||
})
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
{
|
||||
"queue": [
|
||||
ErrorDetail(string="This field is required.", code="required")
|
||||
],
|
||||
"title": [
|
||||
ErrorDetail(string="This field is required.", code="required")
|
||||
],
|
||||
},
|
||||
)
|
||||
self.assertFalse(Ticket.objects.exists())
|
||||
|
||||
def test_create_api_ticket_wrong_date_format(self):
|
||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
||||
staff_user = User.objects.create_user(username="test", is_staff=True)
|
||||
self.client.force_authenticate(staff_user)
|
||||
response = self.client.post('/api/tickets/', {
|
||||
'queue': self.queue.id,
|
||||
'title': 'Test title',
|
||||
'due_date': 'monday, 1st of may 2022'
|
||||
})
|
||||
response = self.client.post(
|
||||
"/api/tickets/",
|
||||
{
|
||||
"queue": self.queue.id,
|
||||
"title": "Test title",
|
||||
"due_date": "monday, 1st of may 2022",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(response.data, {
|
||||
'due_date': [ErrorDetail(string='Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].', code='invalid')]
|
||||
})
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
{
|
||||
"due_date": [
|
||||
ErrorDetail(
|
||||
string="Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].",
|
||||
code="invalid",
|
||||
)
|
||||
]
|
||||
},
|
||||
)
|
||||
self.assertFalse(Ticket.objects.exists())
|
||||
|
||||
def test_create_api_ticket_authenticated_staff_user(self):
|
||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
||||
staff_user = User.objects.create_user(username="test", is_staff=True)
|
||||
self.client.force_authenticate(staff_user)
|
||||
response = self.client.post('/api/tickets/', {
|
||||
'queue': self.queue.id,
|
||||
'title': 'Test title',
|
||||
'description': 'Test description\nMulti lines',
|
||||
'submitter_email': 'test@mail.com',
|
||||
'priority': 4
|
||||
})
|
||||
response = self.client.post(
|
||||
"/api/tickets/",
|
||||
{
|
||||
"queue": self.queue.id,
|
||||
"title": "Test title",
|
||||
"description": "Test description\nMulti lines",
|
||||
"submitter_email": "test@mail.com",
|
||||
"priority": 4,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||
created_ticket = Ticket.objects.get()
|
||||
self.assertEqual(created_ticket.title, 'Test title')
|
||||
self.assertEqual(created_ticket.description,
|
||||
'Test description\nMulti lines')
|
||||
self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
|
||||
self.assertEqual(created_ticket.title, "Test title")
|
||||
self.assertEqual(created_ticket.description, "Test description\nMulti lines")
|
||||
self.assertEqual(created_ticket.submitter_email, "test@mail.com")
|
||||
self.assertEqual(created_ticket.priority, 4)
|
||||
self.assertEqual(created_ticket.followup_set.count(), 1)
|
||||
|
||||
def test_create_api_ticket_with_basic_auth(self):
|
||||
username = 'admin'
|
||||
password = 'admin'
|
||||
User.objects.create_user(
|
||||
username=username, password=password, is_staff=True)
|
||||
username = "admin"
|
||||
password = "admin"
|
||||
User.objects.create_user(username=username, password=password, is_staff=True)
|
||||
|
||||
test_user = User.objects.create_user(username='test')
|
||||
merge_ticket = Ticket.objects.create(
|
||||
queue=self.queue, title='merge ticket')
|
||||
test_user = User.objects.create_user(username="test")
|
||||
merge_ticket = Ticket.objects.create(queue=self.queue, title="merge ticket")
|
||||
|
||||
# Generate base64 credentials string
|
||||
credentials = f"{username}:{password}"
|
||||
base64_credentials = base64.b64encode(credentials.encode(
|
||||
HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
|
||||
base64_credentials = base64.b64encode(
|
||||
credentials.encode(HTTP_HEADER_ENCODING)
|
||||
).decode(HTTP_HEADER_ENCODING)
|
||||
|
||||
self.client.credentials(
|
||||
HTTP_AUTHORIZATION=f"Basic {base64_credentials}")
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f"Basic {base64_credentials}")
|
||||
response = self.client.post(
|
||||
'/api/tickets/',
|
||||
"/api/tickets/",
|
||||
{
|
||||
'queue': self.queue.id,
|
||||
'title': 'Title',
|
||||
'description': 'Description',
|
||||
'resolution': 'Resolution',
|
||||
'assigned_to': test_user.id,
|
||||
'submitter_email': 'test@mail.com',
|
||||
'status': Ticket.RESOLVED_STATUS,
|
||||
'priority': 1,
|
||||
'on_hold': True,
|
||||
'due_date': self.due_date,
|
||||
'merged_to': merge_ticket.id
|
||||
}
|
||||
"queue": self.queue.id,
|
||||
"title": "Title",
|
||||
"description": "Description",
|
||||
"resolution": "Resolution",
|
||||
"assigned_to": test_user.id,
|
||||
"submitter_email": "test@mail.com",
|
||||
"status": Ticket.RESOLVED_STATUS,
|
||||
"priority": 1,
|
||||
"on_hold": True,
|
||||
"due_date": self.due_date,
|
||||
"merged_to": merge_ticket.id,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||
created_ticket = Ticket.objects.last()
|
||||
self.assertEqual(created_ticket.title, 'Title')
|
||||
self.assertEqual(created_ticket.description, 'Description')
|
||||
self.assertEqual(created_ticket.title, "Title")
|
||||
self.assertEqual(created_ticket.description, "Description")
|
||||
# resolution can not be set on creation
|
||||
self.assertIsNone(created_ticket.resolution)
|
||||
self.assertEqual(created_ticket.assigned_to, test_user)
|
||||
self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
|
||||
self.assertEqual(created_ticket.submitter_email, "test@mail.com")
|
||||
self.assertEqual(created_ticket.priority, 1)
|
||||
# on_hold is False on creation
|
||||
self.assertFalse(created_ticket.on_hold)
|
||||
@ -134,39 +151,37 @@ class TicketTest(APITestCase):
|
||||
self.assertIsNone(created_ticket.merged_to)
|
||||
|
||||
def test_edit_api_ticket(self):
|
||||
staff_user = User.objects.create_user(username='admin', is_staff=True)
|
||||
test_ticket = Ticket.objects.create(
|
||||
queue=self.queue, title='Test ticket')
|
||||
staff_user = User.objects.create_user(username="admin", is_staff=True)
|
||||
test_ticket = Ticket.objects.create(queue=self.queue, title="Test ticket")
|
||||
|
||||
test_user = User.objects.create_user(username='test')
|
||||
merge_ticket = Ticket.objects.create(
|
||||
queue=self.queue, title='merge ticket')
|
||||
test_user = User.objects.create_user(username="test")
|
||||
merge_ticket = Ticket.objects.create(queue=self.queue, title="merge ticket")
|
||||
|
||||
self.client.force_authenticate(staff_user)
|
||||
response = self.client.put(
|
||||
'/api/tickets/%d/' % test_ticket.id,
|
||||
"/api/tickets/%d/" % test_ticket.id,
|
||||
{
|
||||
'queue': self.queue.id,
|
||||
'title': 'Title',
|
||||
'description': 'Description',
|
||||
'resolution': 'Resolution',
|
||||
'assigned_to': test_user.id,
|
||||
'submitter_email': 'test@mail.com',
|
||||
'status': Ticket.RESOLVED_STATUS,
|
||||
'priority': 1,
|
||||
'on_hold': True,
|
||||
'due_date': self.due_date,
|
||||
'merged_to': merge_ticket.id
|
||||
}
|
||||
"queue": self.queue.id,
|
||||
"title": "Title",
|
||||
"description": "Description",
|
||||
"resolution": "Resolution",
|
||||
"assigned_to": test_user.id,
|
||||
"submitter_email": "test@mail.com",
|
||||
"status": Ticket.RESOLVED_STATUS,
|
||||
"priority": 1,
|
||||
"on_hold": True,
|
||||
"due_date": self.due_date,
|
||||
"merged_to": merge_ticket.id,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||
test_ticket.refresh_from_db()
|
||||
self.assertEqual(test_ticket.title, 'Title')
|
||||
self.assertEqual(test_ticket.description, 'Description')
|
||||
self.assertEqual(test_ticket.resolution, 'Resolution')
|
||||
self.assertEqual(test_ticket.title, "Title")
|
||||
self.assertEqual(test_ticket.description, "Description")
|
||||
self.assertEqual(test_ticket.resolution, "Resolution")
|
||||
self.assertEqual(test_ticket.assigned_to, test_user)
|
||||
self.assertEqual(test_ticket.submitter_email, 'test@mail.com')
|
||||
self.assertEqual(test_ticket.submitter_email, "test@mail.com")
|
||||
self.assertEqual(test_ticket.priority, 1)
|
||||
self.assertTrue(test_ticket.on_hold)
|
||||
self.assertEqual(test_ticket.status, Ticket.RESOLVED_STATUS)
|
||||
@ -174,236 +189,264 @@ class TicketTest(APITestCase):
|
||||
self.assertEqual(test_ticket.merged_to, merge_ticket)
|
||||
|
||||
def test_partial_edit_api_ticket(self):
|
||||
staff_user = User.objects.create_user(username='admin', is_staff=True)
|
||||
test_ticket = Ticket.objects.create(
|
||||
queue=self.queue, title='Test ticket')
|
||||
staff_user = User.objects.create_user(username="admin", is_staff=True)
|
||||
test_ticket = Ticket.objects.create(queue=self.queue, title="Test ticket")
|
||||
|
||||
self.client.force_authenticate(staff_user)
|
||||
response = self.client.patch(
|
||||
'/api/tickets/%d/' % test_ticket.id,
|
||||
"/api/tickets/%d/" % test_ticket.id,
|
||||
{
|
||||
'description': 'New description',
|
||||
}
|
||||
"description": "New description",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||
test_ticket.refresh_from_db()
|
||||
self.assertEqual(test_ticket.description, 'New description')
|
||||
self.assertEqual(test_ticket.description, "New description")
|
||||
|
||||
def test_delete_api_ticket(self):
|
||||
staff_user = User.objects.create_user(username='admin', is_staff=True)
|
||||
test_ticket = Ticket.objects.create(
|
||||
queue=self.queue, title='Test ticket')
|
||||
staff_user = User.objects.create_user(username="admin", is_staff=True)
|
||||
test_ticket = Ticket.objects.create(queue=self.queue, title="Test ticket")
|
||||
self.client.force_authenticate(staff_user)
|
||||
response = self.client.delete('/api/tickets/%d/' % test_ticket.id)
|
||||
response = self.client.delete("/api/tickets/%d/" % test_ticket.id)
|
||||
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
|
||||
self.assertFalse(Ticket.objects.exists())
|
||||
|
||||
@freeze_time('2022-06-30 23:09:44')
|
||||
@freeze_time("2022-06-30 23:09:44")
|
||||
def test_create_api_ticket_with_custom_fields(self):
|
||||
# Create custom fields
|
||||
for field_type, field_display in CustomField.DATA_TYPE_CHOICES:
|
||||
extra_data = {}
|
||||
if field_type in ('varchar', 'text'):
|
||||
extra_data['max_length'] = 10
|
||||
if field_type == 'integer':
|
||||
if field_type in ("varchar", "text"):
|
||||
extra_data["max_length"] = 10
|
||||
if field_type == "integer":
|
||||
# Set one field as required to test error if not provided
|
||||
extra_data['required'] = True
|
||||
if field_type == 'decimal':
|
||||
extra_data['max_length'] = 7
|
||||
extra_data['decimal_places'] = 3
|
||||
if field_type == 'list':
|
||||
extra_data['list_values'] = '''Green
|
||||
extra_data["required"] = True
|
||||
if field_type == "decimal":
|
||||
extra_data["max_length"] = 7
|
||||
extra_data["decimal_places"] = 3
|
||||
if field_type == "list":
|
||||
extra_data["list_values"] = """Green
|
||||
Blue
|
||||
Red
|
||||
Yellow'''
|
||||
Yellow"""
|
||||
CustomField.objects.create(
|
||||
name=field_type, label=field_display, data_type=field_type, **extra_data)
|
||||
name=field_type, label=field_display, data_type=field_type, **extra_data
|
||||
)
|
||||
|
||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
||||
staff_user = User.objects.create_user(username="test", is_staff=True)
|
||||
self.client.force_authenticate(staff_user)
|
||||
|
||||
# Test creation without providing required field
|
||||
response = self.client.post('/api/tickets/', {
|
||||
'queue': self.queue.id,
|
||||
'title': 'Test title',
|
||||
'description': 'Test description\nMulti lines',
|
||||
'submitter_email': 'test@mail.com',
|
||||
'priority': 4
|
||||
})
|
||||
response = self.client.post(
|
||||
"/api/tickets/",
|
||||
{
|
||||
"queue": self.queue.id,
|
||||
"title": "Test title",
|
||||
"description": "Test description\nMulti lines",
|
||||
"submitter_email": "test@mail.com",
|
||||
"priority": 4,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(response.data, {'custom_integer': [ErrorDetail(
|
||||
string='This field is required.', code='required')]})
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
{
|
||||
"custom_integer": [
|
||||
ErrorDetail(string="This field is required.", code="required")
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
# Test creation with custom field values
|
||||
response = self.client.post('/api/tickets/', {
|
||||
'queue': self.queue.id,
|
||||
'title': 'Test title',
|
||||
'description': 'Test description\nMulti lines',
|
||||
'submitter_email': 'test@mail.com',
|
||||
'priority': 4,
|
||||
'custom_varchar': 'test',
|
||||
'custom_text': 'multi\nline',
|
||||
'custom_integer': '1',
|
||||
'custom_decimal': '42.987',
|
||||
'custom_list': 'Red',
|
||||
'custom_boolean': True,
|
||||
'custom_date': '2022-4-11',
|
||||
'custom_time': '23:59:59',
|
||||
'custom_datetime': '2022-4-10 18:27',
|
||||
'custom_email': 'email@test.com',
|
||||
'custom_url': 'http://django-helpdesk.readthedocs.org/',
|
||||
'custom_ipaddress': '127.0.0.1',
|
||||
'custom_slug': 'test-slug',
|
||||
})
|
||||
response = self.client.post(
|
||||
"/api/tickets/",
|
||||
{
|
||||
"queue": self.queue.id,
|
||||
"title": "Test title",
|
||||
"description": "Test description\nMulti lines",
|
||||
"submitter_email": "test@mail.com",
|
||||
"priority": 4,
|
||||
"custom_varchar": "test",
|
||||
"custom_text": "multi\nline",
|
||||
"custom_integer": "1",
|
||||
"custom_decimal": "42.987",
|
||||
"custom_list": "Red",
|
||||
"custom_boolean": True,
|
||||
"custom_date": "2022-4-11",
|
||||
"custom_time": "23:59:59",
|
||||
"custom_datetime": "2022-4-10 18:27",
|
||||
"custom_email": "email@test.com",
|
||||
"custom_url": "http://django-helpdesk.readthedocs.org/",
|
||||
"custom_ipaddress": "127.0.0.1",
|
||||
"custom_slug": "test-slug",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||
# Check all fields with data returned from the response
|
||||
self.assertEqual(response.data, {
|
||||
'id': 1,
|
||||
'queue': 1,
|
||||
'title': 'Test title',
|
||||
'description': 'Test description\nMulti lines',
|
||||
'resolution': None,
|
||||
'submitter_email': 'test@mail.com',
|
||||
'assigned_to': None,
|
||||
'status': 1,
|
||||
'on_hold': False,
|
||||
'priority': 4,
|
||||
'due_date': None,
|
||||
'merged_to': None,
|
||||
'followup_set': [OrderedDict([
|
||||
('id', 1),
|
||||
('ticket', 1),
|
||||
('user', 1),
|
||||
('title', 'Ticket Opened'),
|
||||
('comment', 'Test description\nMulti lines'),
|
||||
('public', True),
|
||||
('new_status', None),
|
||||
('time_spent', None),
|
||||
('followupattachment_set', []),
|
||||
('date', '2022-06-30T23:09:44'),
|
||||
('message_id', None),
|
||||
])],
|
||||
'custom_varchar': 'test',
|
||||
'custom_text': 'multi\nline',
|
||||
'custom_integer': 1,
|
||||
'custom_decimal': '42.987',
|
||||
'custom_list': 'Red',
|
||||
'custom_boolean': True,
|
||||
'custom_date': '2022-04-11',
|
||||
'custom_time': '23:59:59',
|
||||
'custom_datetime': '2022-04-10T18:27',
|
||||
'custom_email': 'email@test.com',
|
||||
'custom_url': 'http://django-helpdesk.readthedocs.org/',
|
||||
'custom_ipaddress': '127.0.0.1',
|
||||
'custom_slug': 'test-slug'
|
||||
})
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
{
|
||||
"id": 1,
|
||||
"queue": 1,
|
||||
"title": "Test title",
|
||||
"description": "Test description\nMulti lines",
|
||||
"resolution": None,
|
||||
"submitter_email": "test@mail.com",
|
||||
"assigned_to": None,
|
||||
"status": 1,
|
||||
"on_hold": False,
|
||||
"priority": 4,
|
||||
"due_date": None,
|
||||
"merged_to": None,
|
||||
"followup_set": [
|
||||
OrderedDict(
|
||||
[
|
||||
("id", 1),
|
||||
("ticket", 1),
|
||||
("user", 1),
|
||||
("title", "Ticket Opened"),
|
||||
("comment", "Test description\nMulti lines"),
|
||||
("public", True),
|
||||
("new_status", None),
|
||||
("time_spent", None),
|
||||
("followupattachment_set", []),
|
||||
("date", "2022-06-30T23:09:44"),
|
||||
("message_id", None),
|
||||
]
|
||||
)
|
||||
],
|
||||
"custom_varchar": "test",
|
||||
"custom_text": "multi\nline",
|
||||
"custom_integer": 1,
|
||||
"custom_decimal": "42.987",
|
||||
"custom_list": "Red",
|
||||
"custom_boolean": True,
|
||||
"custom_date": "2022-04-11",
|
||||
"custom_time": "23:59:59",
|
||||
"custom_datetime": "2022-04-10T18:27",
|
||||
"custom_email": "email@test.com",
|
||||
"custom_url": "http://django-helpdesk.readthedocs.org/",
|
||||
"custom_ipaddress": "127.0.0.1",
|
||||
"custom_slug": "test-slug",
|
||||
},
|
||||
)
|
||||
|
||||
def test_create_api_ticket_with_attachment(self):
|
||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
||||
staff_user = User.objects.create_user(username="test", is_staff=True)
|
||||
self.client.force_authenticate(staff_user)
|
||||
test_file = SimpleUploadedFile(
|
||||
'file.jpg', b'file_content', content_type='image/jpg')
|
||||
response = self.client.post('/api/tickets/', {
|
||||
'queue': self.queue.id,
|
||||
'title': 'Test title',
|
||||
'description': 'Test description\nMulti lines',
|
||||
'submitter_email': 'test@mail.com',
|
||||
'priority': 4,
|
||||
'attachment': test_file
|
||||
})
|
||||
"file.jpg", b"file_content", content_type="image/jpg"
|
||||
)
|
||||
response = self.client.post(
|
||||
"/api/tickets/",
|
||||
{
|
||||
"queue": self.queue.id,
|
||||
"title": "Test title",
|
||||
"description": "Test description\nMulti lines",
|
||||
"submitter_email": "test@mail.com",
|
||||
"priority": 4,
|
||||
"attachment": test_file,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||
created_ticket = Ticket.objects.get()
|
||||
self.assertEqual(created_ticket.title, 'Test title')
|
||||
self.assertEqual(created_ticket.description,
|
||||
'Test description\nMulti lines')
|
||||
self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
|
||||
self.assertEqual(created_ticket.title, "Test title")
|
||||
self.assertEqual(created_ticket.description, "Test description\nMulti lines")
|
||||
self.assertEqual(created_ticket.submitter_email, "test@mail.com")
|
||||
self.assertEqual(created_ticket.priority, 4)
|
||||
self.assertEqual(created_ticket.followup_set.count(), 1)
|
||||
self.assertEqual(created_ticket.followup_set.get(
|
||||
).followupattachment_set.count(), 1)
|
||||
self.assertEqual(
|
||||
created_ticket.followup_set.get().followupattachment_set.count(), 1
|
||||
)
|
||||
attachment = created_ticket.followup_set.get().followupattachment_set.get()
|
||||
self.assertEqual(
|
||||
attachment.file.name,
|
||||
f'helpdesk/attachments/test-queue-1-{created_ticket.secret_key}/1/file.jpg'
|
||||
f"helpdesk/attachments/test-queue-1-{created_ticket.secret_key}/1/file.jpg",
|
||||
)
|
||||
|
||||
def test_create_follow_up_with_attachments(self):
|
||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
||||
staff_user = User.objects.create_user(username="test", is_staff=True)
|
||||
self.client.force_authenticate(staff_user)
|
||||
ticket = Ticket.objects.create(queue=self.queue, title='Test')
|
||||
ticket = Ticket.objects.create(queue=self.queue, title="Test")
|
||||
test_file_1 = SimpleUploadedFile(
|
||||
'file.jpg', b'file_content', content_type='image/jpg')
|
||||
"file.jpg", b"file_content", content_type="image/jpg"
|
||||
)
|
||||
test_file_2 = SimpleUploadedFile(
|
||||
'doc.pdf', b'Doc content', content_type='application/pdf')
|
||||
"doc.pdf", b"Doc content", content_type="application/pdf"
|
||||
)
|
||||
|
||||
response = self.client.post('/api/followups/', {
|
||||
'ticket': ticket.id,
|
||||
'title': 'Test',
|
||||
'comment': 'Test answer\nMulti lines',
|
||||
'attachments': [
|
||||
test_file_1,
|
||||
test_file_2
|
||||
]
|
||||
})
|
||||
response = self.client.post(
|
||||
"/api/followups/",
|
||||
{
|
||||
"ticket": ticket.id,
|
||||
"title": "Test",
|
||||
"comment": "Test answer\nMulti lines",
|
||||
"attachments": [test_file_1, test_file_2],
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||
created_followup = ticket.followup_set.last()
|
||||
self.assertEqual(created_followup.title, 'Test')
|
||||
self.assertEqual(created_followup.comment, 'Test answer\nMulti lines')
|
||||
self.assertEqual(created_followup.title, "Test")
|
||||
self.assertEqual(created_followup.comment, "Test answer\nMulti lines")
|
||||
self.assertEqual(created_followup.followupattachment_set.count(), 2)
|
||||
self.assertEqual(
|
||||
created_followup.followupattachment_set.first().filename, 'doc.pdf')
|
||||
created_followup.followupattachment_set.first().filename, "doc.pdf"
|
||||
)
|
||||
self.assertEqual(
|
||||
created_followup.followupattachment_set.first().mime_type, 'application/pdf')
|
||||
created_followup.followupattachment_set.first().mime_type, "application/pdf"
|
||||
)
|
||||
self.assertEqual(
|
||||
created_followup.followupattachment_set.last().filename, 'file.jpg')
|
||||
created_followup.followupattachment_set.last().filename, "file.jpg"
|
||||
)
|
||||
self.assertEqual(
|
||||
created_followup.followupattachment_set.last().mime_type, 'image/jpg')
|
||||
created_followup.followupattachment_set.last().mime_type, "image/jpg"
|
||||
)
|
||||
|
||||
|
||||
class UserTicketTest(APITestCase):
|
||||
def setUp(self):
|
||||
self.queue = Queue.objects.create(title='Test queue')
|
||||
self.user = User.objects.create_user(username='test')
|
||||
self.queue = Queue.objects.create(title="Test queue")
|
||||
self.user = User.objects.create_user(username="test")
|
||||
self.client.force_authenticate(self.user)
|
||||
|
||||
def test_get_user_tickets(self):
|
||||
user = User.objects.create_user(username='test2', email="foo@example.com")
|
||||
user = User.objects.create_user(username="test2", email="foo@example.com")
|
||||
ticket_1 = Ticket.objects.create(
|
||||
queue=self.queue, title='Test 1',
|
||||
submitter_email="foo@example.com")
|
||||
queue=self.queue, title="Test 1", submitter_email="foo@example.com"
|
||||
)
|
||||
ticket_2 = Ticket.objects.create(
|
||||
queue=self.queue, title='Test 2',
|
||||
submitter_email="bar@example.com")
|
||||
queue=self.queue, title="Test 2", submitter_email="bar@example.com"
|
||||
)
|
||||
ticket_3 = Ticket.objects.create(
|
||||
queue=self.queue, title='Test 3',
|
||||
submitter_email="foo@example.com")
|
||||
queue=self.queue, title="Test 3", submitter_email="foo@example.com"
|
||||
)
|
||||
self.client.force_authenticate(user)
|
||||
response = self.client.get('/api/user_tickets/')
|
||||
response = self.client.get("/api/user_tickets/")
|
||||
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||
self.assertEqual(len(response.data["results"]), 2)
|
||||
self.assertEqual(response.data["results"][0]['id'], ticket_3.id)
|
||||
self.assertEqual(response.data["results"][1]['id'], ticket_1.id)
|
||||
self.assertEqual(response.data["results"][0]["id"], ticket_3.id)
|
||||
self.assertEqual(response.data["results"][1]["id"], ticket_1.id)
|
||||
|
||||
def test_staff_user(self):
|
||||
staff_user = User.objects.create_user(username='test2', is_staff=True, email="staff@example.com")
|
||||
staff_user = User.objects.create_user(
|
||||
username="test2", is_staff=True, email="staff@example.com"
|
||||
)
|
||||
ticket_1 = Ticket.objects.create(
|
||||
queue=self.queue, title='Test 1',
|
||||
submitter_email="staff@example.com")
|
||||
queue=self.queue, title="Test 1", submitter_email="staff@example.com"
|
||||
)
|
||||
ticket_2 = Ticket.objects.create(
|
||||
queue=self.queue, title='Test 2',
|
||||
submitter_email="foo@example.com")
|
||||
queue=self.queue, title="Test 2", submitter_email="foo@example.com"
|
||||
)
|
||||
self.client.force_authenticate(staff_user)
|
||||
response = self.client.get('/api/user_tickets/')
|
||||
response = self.client.get("/api/user_tickets/")
|
||||
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||
self.assertEqual(len(response.data["results"]), 1)
|
||||
|
||||
def test_not_logged_in_user(self):
|
||||
ticket_1 = Ticket.objects.create(
|
||||
queue=self.queue, title='Test 1',
|
||||
submitter_email="ex@example.com")
|
||||
queue=self.queue, title="Test 1", submitter_email="ex@example.com"
|
||||
)
|
||||
self.client.logout()
|
||||
response = self.client.get('/api/user_tickets/')
|
||||
response = self.client.get("/api/user_tickets/")
|
||||
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
|
||||
|
||||
|
||||
|
@ -12,162 +12,161 @@ from unittest import mock
|
||||
from unittest.case import skip
|
||||
|
||||
|
||||
MEDIA_DIR = os.path.join(gettempdir(), 'helpdesk_test_media')
|
||||
MEDIA_DIR = os.path.join(gettempdir(), "helpdesk_test_media")
|
||||
|
||||
|
||||
@override_settings(MEDIA_ROOT=MEDIA_DIR)
|
||||
class AttachmentIntegrationTests(TestCase):
|
||||
|
||||
fixtures = ['emailtemplate.json']
|
||||
fixtures = ["emailtemplate.json"]
|
||||
|
||||
def setUp(self):
|
||||
self.queue_public = models.Queue.objects.create(
|
||||
title='Public Queue',
|
||||
slug='pub_q',
|
||||
title="Public Queue",
|
||||
slug="pub_q",
|
||||
allow_public_submission=True,
|
||||
new_ticket_cc='new.public@example.com',
|
||||
updated_ticket_cc='update.public@example.com',
|
||||
new_ticket_cc="new.public@example.com",
|
||||
updated_ticket_cc="update.public@example.com",
|
||||
)
|
||||
|
||||
self.queue_private = models.Queue.objects.create(
|
||||
title='Private Queue',
|
||||
slug='priv_q',
|
||||
title="Private Queue",
|
||||
slug="priv_q",
|
||||
allow_public_submission=False,
|
||||
new_ticket_cc='new.private@example.com',
|
||||
updated_ticket_cc='update.private@example.com',
|
||||
new_ticket_cc="new.private@example.com",
|
||||
updated_ticket_cc="update.private@example.com",
|
||||
)
|
||||
|
||||
self.ticket_data = {
|
||||
'title': 'Test Ticket Title',
|
||||
'body': 'Test Ticket Desc',
|
||||
'priority': 3,
|
||||
'submitter_email': 'submitter@example.com',
|
||||
"title": "Test Ticket Title",
|
||||
"body": "Test Ticket Desc",
|
||||
"priority": 3,
|
||||
"submitter_email": "submitter@example.com",
|
||||
}
|
||||
|
||||
def test_create_pub_ticket_with_attachment(self):
|
||||
test_file = SimpleUploadedFile(
|
||||
'test_att.txt', b'attached file content', 'text/plain')
|
||||
"test_att.txt", b"attached file content", "text/plain"
|
||||
)
|
||||
post_data = self.ticket_data.copy()
|
||||
post_data.update({
|
||||
'queue': self.queue_public.id,
|
||||
'attachment': test_file,
|
||||
})
|
||||
post_data.update(
|
||||
{
|
||||
"queue": self.queue_public.id,
|
||||
"attachment": test_file,
|
||||
}
|
||||
)
|
||||
|
||||
# Ensure ticket form submits with attachment successfully
|
||||
response = self.client.post(
|
||||
reverse('helpdesk:home'), post_data, follow=True)
|
||||
response = self.client.post(reverse("helpdesk:home"), post_data, follow=True)
|
||||
self.assertContains(response, test_file.name)
|
||||
|
||||
# Ensure attachment is available with correct content
|
||||
att = models.FollowUpAttachment.objects.get(
|
||||
followup__ticket=response.context['ticket'])
|
||||
followup__ticket=response.context["ticket"]
|
||||
)
|
||||
with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk:
|
||||
disk_content = file_on_disk.read()
|
||||
self.assertEqual(disk_content, 'attached file content')
|
||||
self.assertEqual(disk_content, "attached file content")
|
||||
|
||||
def test_create_pub_ticket_with_attachment_utf8(self):
|
||||
test_file = SimpleUploadedFile(
|
||||
'ß°äöü.txt', 'โจ'.encode('utf-8'), 'text/utf-8')
|
||||
test_file = SimpleUploadedFile("ß°äöü.txt", "โจ".encode("utf-8"), "text/utf-8")
|
||||
post_data = self.ticket_data.copy()
|
||||
post_data.update({
|
||||
'queue': self.queue_public.id,
|
||||
'attachment': test_file,
|
||||
})
|
||||
post_data.update(
|
||||
{
|
||||
"queue": self.queue_public.id,
|
||||
"attachment": test_file,
|
||||
}
|
||||
)
|
||||
|
||||
# Ensure ticket form submits with attachment successfully
|
||||
response = self.client.post(
|
||||
reverse('helpdesk:home'), post_data, follow=True)
|
||||
response = self.client.post(reverse("helpdesk:home"), post_data, follow=True)
|
||||
self.assertContains(response, test_file.name)
|
||||
|
||||
# Ensure attachment is available with correct content
|
||||
att = models.FollowUpAttachment.objects.get(
|
||||
followup__ticket=response.context['ticket'])
|
||||
with open(os.path.join(MEDIA_DIR, att.file.name), encoding="utf-8") as file_on_disk:
|
||||
disk_content = smart_str(file_on_disk.read(), 'utf-8')
|
||||
self.assertEqual(disk_content, 'โจ')
|
||||
followup__ticket=response.context["ticket"]
|
||||
)
|
||||
with open(
|
||||
os.path.join(MEDIA_DIR, att.file.name), encoding="utf-8"
|
||||
) as file_on_disk:
|
||||
disk_content = smart_str(file_on_disk.read(), "utf-8")
|
||||
self.assertEqual(disk_content, "โจ")
|
||||
|
||||
|
||||
@mock.patch.object(models.FollowUp, 'save', autospec=True)
|
||||
@mock.patch.object(models.FollowUpAttachment, 'save', autospec=True)
|
||||
@mock.patch.object(models.Ticket, 'save', autospec=True)
|
||||
@mock.patch.object(models.Queue, 'save', autospec=True)
|
||||
@mock.patch.object(models.FollowUp, "save", autospec=True)
|
||||
@mock.patch.object(models.FollowUpAttachment, "save", autospec=True)
|
||||
@mock.patch.object(models.Ticket, "save", autospec=True)
|
||||
@mock.patch.object(models.Queue, "save", autospec=True)
|
||||
class AttachmentUnitTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.file_attrs = {
|
||||
'filename': '°ßäöü.txt',
|
||||
'content': 'โจ'.encode('utf-8'),
|
||||
'content-type': 'text/utf8',
|
||||
"filename": "°ßäöü.txt",
|
||||
"content": "โจ".encode("utf-8"),
|
||||
"content-type": "text/utf8",
|
||||
}
|
||||
self.test_file = SimpleUploadedFile.from_dict(self.file_attrs)
|
||||
self.follow_up = models.FollowUp.objects.create(
|
||||
ticket=models.Ticket.objects.create(
|
||||
queue=models.Queue.objects.create()
|
||||
)
|
||||
ticket=models.Ticket.objects.create(queue=models.Queue.objects.create())
|
||||
)
|
||||
|
||||
@skip("Rework with model relocation")
|
||||
def test_unicode_attachment_filename(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
|
||||
""" check utf-8 data is parsed correctly """
|
||||
filename, fileobj = lib.process_attachments(
|
||||
self.follow_up, [self.test_file])[0]
|
||||
def test_unicode_attachment_filename(
|
||||
self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save
|
||||
):
|
||||
"""check utf-8 data is parsed correctly"""
|
||||
filename, fileobj = lib.process_attachments(self.follow_up, [self.test_file])[0]
|
||||
mock_att_save.assert_called_with(
|
||||
file=self.test_file,
|
||||
filename=self.file_attrs['filename'],
|
||||
mime_type=self.file_attrs['content-type'],
|
||||
size=len(self.file_attrs['content']),
|
||||
followup=self.follow_up
|
||||
)
|
||||
self.assertEqual(filename, self.file_attrs['filename'])
|
||||
|
||||
def test_autofill(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
|
||||
""" check utf-8 data is parsed correctly """
|
||||
obj = models.FollowUpAttachment.objects.create(
|
||||
filename=self.file_attrs["filename"],
|
||||
mime_type=self.file_attrs["content-type"],
|
||||
size=len(self.file_attrs["content"]),
|
||||
followup=self.follow_up,
|
||||
file=self.test_file
|
||||
)
|
||||
self.assertEqual(filename, self.file_attrs["filename"])
|
||||
|
||||
def test_autofill(
|
||||
self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save
|
||||
):
|
||||
"""check utf-8 data is parsed correctly"""
|
||||
obj = models.FollowUpAttachment.objects.create(
|
||||
followup=self.follow_up, file=self.test_file
|
||||
)
|
||||
obj.save()
|
||||
self.assertEqual(obj.file.name, self.file_attrs['filename'])
|
||||
self.assertEqual(obj.file.size, len(self.file_attrs['content']))
|
||||
self.assertEqual(obj.file.name, self.file_attrs["filename"])
|
||||
self.assertEqual(obj.file.size, len(self.file_attrs["content"]))
|
||||
self.assertEqual(obj.file.file.content_type, "text/utf8")
|
||||
|
||||
def test_kbi_attachment(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
|
||||
""" check utf-8 data is parsed correctly """
|
||||
def test_kbi_attachment(
|
||||
self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save
|
||||
):
|
||||
"""check utf-8 data is parsed correctly"""
|
||||
|
||||
kbcategory = models.KBCategory.objects.create(
|
||||
title="Title",
|
||||
slug="slug",
|
||||
description="Description"
|
||||
title="Title", slug="slug", description="Description"
|
||||
)
|
||||
kbitem = models.KBItem.objects.create(
|
||||
category=kbcategory,
|
||||
title="Title",
|
||||
question="Question",
|
||||
answer="Answer"
|
||||
category=kbcategory, title="Title", question="Question", answer="Answer"
|
||||
)
|
||||
|
||||
obj = models.KBIAttachment.objects.create(
|
||||
kbitem=kbitem,
|
||||
file=self.test_file
|
||||
)
|
||||
obj = models.KBIAttachment.objects.create(kbitem=kbitem, file=self.test_file)
|
||||
obj.save()
|
||||
self.assertEqual(obj.filename, self.file_attrs['filename'])
|
||||
self.assertEqual(obj.file.size, len(self.file_attrs['content']))
|
||||
self.assertEqual(obj.filename, self.file_attrs["filename"])
|
||||
self.assertEqual(obj.file.size, len(self.file_attrs["content"]))
|
||||
self.assertEqual(obj.mime_type, "text/plain")
|
||||
|
||||
@skip("model in lib not patched")
|
||||
@override_settings(MEDIA_ROOT=MEDIA_DIR)
|
||||
def test_unicode_filename_to_filesystem(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
|
||||
""" don't mock saving to filesystem to test file renames caused by storage layer """
|
||||
filename, fileobj = lib.process_attachments(
|
||||
self.follow_up, [self.test_file])[0]
|
||||
def test_unicode_filename_to_filesystem(
|
||||
self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save
|
||||
):
|
||||
"""don't mock saving to filesystem to test file renames caused by storage layer"""
|
||||
filename, fileobj = lib.process_attachments(self.follow_up, [self.test_file])[0]
|
||||
# Attachment object was zeroth positional arg (i.e. self) of att.save
|
||||
# call
|
||||
attachment_obj = mock_att_save.return_value
|
||||
|
||||
mock_att_save.assert_called_once_with(attachment_obj)
|
||||
self.assertIsInstance(attachment_obj, models.FollowUpAttachment)
|
||||
self.assertEqual(attachment_obj.filename, self.file_attrs['filename'])
|
||||
self.assertEqual(attachment_obj.filename, self.file_attrs["filename"])
|
||||
|
||||
|
||||
def tearDownModule():
|
||||
|
@ -8,245 +8,254 @@ from helpdesk.models import Checklist, ChecklistTask, ChecklistTemplate, Queue,
|
||||
class TicketChecklistTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
user = get_user_model().objects.create_user('User', password='pass')
|
||||
user = get_user_model().objects.create_user("User", password="pass")
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
cls.user = user
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.client.login(username='User', password='pass')
|
||||
self.client.login(username="User", password="pass")
|
||||
|
||||
self.ticket = Ticket.objects.create(queue=Queue.objects.create(title='Queue', slug='queue'))
|
||||
self.ticket = Ticket.objects.create(
|
||||
queue=Queue.objects.create(title="Queue", slug="queue")
|
||||
)
|
||||
|
||||
def test_create_checklist(self):
|
||||
self.assertEqual(self.ticket.checklists.count(), 0)
|
||||
checklist_name = 'test empty checklist'
|
||||
checklist_name = "test empty checklist"
|
||||
|
||||
response = self.client.post(
|
||||
reverse('helpdesk:view', kwargs={'ticket_id': self.ticket.id}),
|
||||
data={'name': checklist_name},
|
||||
follow=True
|
||||
reverse("helpdesk:view", kwargs={"ticket_id": self.ticket.id}),
|
||||
data={"name": checklist_name},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'helpdesk/checklist_form.html')
|
||||
self.assertTemplateUsed(response, "helpdesk/checklist_form.html")
|
||||
self.assertContains(response, checklist_name)
|
||||
|
||||
self.assertEqual(self.ticket.checklists.count(), 1)
|
||||
|
||||
def test_create_checklist_from_template(self):
|
||||
self.assertEqual(self.ticket.checklists.count(), 0)
|
||||
checklist_name = 'test checklist from template'
|
||||
checklist_name = "test checklist from template"
|
||||
|
||||
checklist_template = ChecklistTemplate.objects.create(
|
||||
name='Test template',
|
||||
task_list=['first', 'second', 'last']
|
||||
name="Test template", task_list=["first", "second", "last"]
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('helpdesk:view', kwargs={'ticket_id': self.ticket.id}),
|
||||
data={'name': checklist_name, 'checklist_template': checklist_template.id},
|
||||
follow=True
|
||||
reverse("helpdesk:view", kwargs={"ticket_id": self.ticket.id}),
|
||||
data={"name": checklist_name, "checklist_template": checklist_template.id},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'helpdesk/checklist_form.html')
|
||||
self.assertTemplateUsed(response, "helpdesk/checklist_form.html")
|
||||
self.assertContains(response, checklist_name)
|
||||
|
||||
self.assertEqual(self.ticket.checklists.count(), 1)
|
||||
created_checklist = self.ticket.checklists.get()
|
||||
self.assertEqual(created_checklist.tasks.count(), 3)
|
||||
self.assertEqual(created_checklist.tasks.all()[0].description, 'first')
|
||||
self.assertEqual(created_checklist.tasks.all()[1].description, 'second')
|
||||
self.assertEqual(created_checklist.tasks.all()[2].description, 'last')
|
||||
self.assertEqual(created_checklist.tasks.all()[0].description, "first")
|
||||
self.assertEqual(created_checklist.tasks.all()[1].description, "second")
|
||||
self.assertEqual(created_checklist.tasks.all()[2].description, "last")
|
||||
|
||||
def test_edit_checklist(self):
|
||||
checklist = self.ticket.checklists.create(name='Test checklist')
|
||||
first_task = checklist.tasks.create(description='First task', position=1)
|
||||
checklist.tasks.create(description='To delete task', position=2)
|
||||
checklist = self.ticket.checklists.create(name="Test checklist")
|
||||
first_task = checklist.tasks.create(description="First task", position=1)
|
||||
checklist.tasks.create(description="To delete task", position=2)
|
||||
|
||||
url = reverse('helpdesk:edit_ticket_checklist', kwargs={
|
||||
'ticket_id': self.ticket.id,
|
||||
'checklist_id': checklist.id,
|
||||
})
|
||||
url = reverse(
|
||||
"helpdesk:edit_ticket_checklist",
|
||||
kwargs={
|
||||
"ticket_id": self.ticket.id,
|
||||
"checklist_id": checklist.id,
|
||||
},
|
||||
)
|
||||
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'helpdesk/checklist_form.html')
|
||||
self.assertContains(response, 'Test checklist')
|
||||
self.assertContains(response, 'First task')
|
||||
self.assertContains(response, 'To delete task')
|
||||
self.assertTemplateUsed(response, "helpdesk/checklist_form.html")
|
||||
self.assertContains(response, "Test checklist")
|
||||
self.assertContains(response, "First task")
|
||||
self.assertContains(response, "To delete task")
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
data={
|
||||
'name': 'New name',
|
||||
'tasks-TOTAL_FORMS': 3,
|
||||
'tasks-INITIAL_FORMS': 2,
|
||||
'tasks-0-id': '1',
|
||||
'tasks-0-description': 'First task edited',
|
||||
'tasks-0-position': '2',
|
||||
'tasks-1-id': '2',
|
||||
'tasks-1-description': 'To delete task',
|
||||
'tasks-1-DELETE': 'on',
|
||||
'tasks-1-position': '2',
|
||||
'tasks-2-description': 'New first task',
|
||||
'tasks-2-position': '1',
|
||||
"name": "New name",
|
||||
"tasks-TOTAL_FORMS": 3,
|
||||
"tasks-INITIAL_FORMS": 2,
|
||||
"tasks-0-id": "1",
|
||||
"tasks-0-description": "First task edited",
|
||||
"tasks-0-position": "2",
|
||||
"tasks-1-id": "2",
|
||||
"tasks-1-description": "To delete task",
|
||||
"tasks-1-DELETE": "on",
|
||||
"tasks-1-position": "2",
|
||||
"tasks-2-description": "New first task",
|
||||
"tasks-2-position": "1",
|
||||
},
|
||||
follow=True
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'helpdesk/ticket.html')
|
||||
self.assertTemplateUsed(response, "helpdesk/ticket.html")
|
||||
|
||||
checklist.refresh_from_db()
|
||||
self.assertEqual(checklist.name, 'New name')
|
||||
self.assertEqual(checklist.name, "New name")
|
||||
self.assertEqual(checklist.tasks.count(), 2)
|
||||
first_task.refresh_from_db()
|
||||
self.assertEqual(first_task.description, 'First task edited')
|
||||
self.assertEqual(checklist.tasks.all()[0].description, 'New first task')
|
||||
self.assertEqual(checklist.tasks.all()[1].description, 'First task edited')
|
||||
self.assertEqual(first_task.description, "First task edited")
|
||||
self.assertEqual(checklist.tasks.all()[0].description, "New first task")
|
||||
self.assertEqual(checklist.tasks.all()[1].description, "First task edited")
|
||||
|
||||
def test_delete_checklist(self):
|
||||
checklist = self.ticket.checklists.create(name='Test checklist')
|
||||
checklist.tasks.create(description='First task', position=1)
|
||||
checklist = self.ticket.checklists.create(name="Test checklist")
|
||||
checklist.tasks.create(description="First task", position=1)
|
||||
self.assertEqual(Checklist.objects.count(), 1)
|
||||
self.assertEqual(ChecklistTask.objects.count(), 1)
|
||||
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
'helpdesk:delete_ticket_checklist',
|
||||
kwargs={'ticket_id': self.ticket.id, 'checklist_id': checklist.id}
|
||||
"helpdesk:delete_ticket_checklist",
|
||||
kwargs={"ticket_id": self.ticket.id, "checklist_id": checklist.id},
|
||||
),
|
||||
follow=True
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'helpdesk/ticket.html')
|
||||
self.assertTemplateUsed(response, "helpdesk/ticket.html")
|
||||
|
||||
self.assertEqual(Checklist.objects.count(), 0)
|
||||
self.assertEqual(ChecklistTask.objects.count(), 0)
|
||||
|
||||
def test_mark_task_as_done(self):
|
||||
checklist = self.ticket.checklists.create(name='Test checklist')
|
||||
task = checklist.tasks.create(description='Task', position=1)
|
||||
checklist = self.ticket.checklists.create(name="Test checklist")
|
||||
task = checklist.tasks.create(description="Task", position=1)
|
||||
self.assertIsNone(task.completion_date)
|
||||
|
||||
self.assertEqual(self.ticket.followup_set.count(), 0)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('helpdesk:update', kwargs={'ticket_id': self.ticket.id}),
|
||||
data={
|
||||
f'checklist-{checklist.id}': task.id
|
||||
},
|
||||
follow=True
|
||||
reverse("helpdesk:update", kwargs={"ticket_id": self.ticket.id}),
|
||||
data={f"checklist-{checklist.id}": task.id},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'helpdesk/ticket.html')
|
||||
self.assertTemplateUsed(response, "helpdesk/ticket.html")
|
||||
|
||||
self.assertEqual(self.ticket.followup_set.count(), 1)
|
||||
followup = self.ticket.followup_set.get()
|
||||
self.assertEqual(followup.ticketchange_set.count(), 1)
|
||||
self.assertEqual(followup.ticketchange_set.get().old_value, 'To do')
|
||||
self.assertEqual(followup.ticketchange_set.get().new_value, 'Completed')
|
||||
self.assertEqual(followup.ticketchange_set.get().old_value, "To do")
|
||||
self.assertEqual(followup.ticketchange_set.get().new_value, "Completed")
|
||||
|
||||
task.refresh_from_db()
|
||||
self.assertIsNotNone(task.completion_date)
|
||||
|
||||
def test_mark_task_as_undone(self):
|
||||
checklist = self.ticket.checklists.create(name='Test checklist')
|
||||
task = checklist.tasks.create(description='Task', position=1, completion_date=datetime(2023, 5, 1))
|
||||
checklist = self.ticket.checklists.create(name="Test checklist")
|
||||
task = checklist.tasks.create(
|
||||
description="Task", position=1, completion_date=datetime(2023, 5, 1)
|
||||
)
|
||||
self.assertIsNotNone(task.completion_date)
|
||||
|
||||
self.assertEqual(self.ticket.followup_set.count(), 0)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('helpdesk:update', kwargs={'ticket_id': self.ticket.id}),
|
||||
follow=True
|
||||
reverse("helpdesk:update", kwargs={"ticket_id": self.ticket.id}),
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'helpdesk/ticket.html')
|
||||
self.assertTemplateUsed(response, "helpdesk/ticket.html")
|
||||
|
||||
self.assertEqual(self.ticket.followup_set.count(), 1)
|
||||
followup = self.ticket.followup_set.get()
|
||||
self.assertEqual(followup.ticketchange_set.count(), 1)
|
||||
self.assertEqual(followup.ticketchange_set.get().old_value, 'Completed')
|
||||
self.assertEqual(followup.ticketchange_set.get().new_value, 'To do')
|
||||
self.assertEqual(followup.ticketchange_set.get().old_value, "Completed")
|
||||
self.assertEqual(followup.ticketchange_set.get().new_value, "To do")
|
||||
|
||||
task.refresh_from_db()
|
||||
self.assertIsNone(task.completion_date)
|
||||
|
||||
def test_display_checklist_templates(self):
|
||||
ChecklistTemplate.objects.create(
|
||||
name='Test checklist template',
|
||||
task_list=['first', 'second', 'third']
|
||||
name="Test checklist template", task_list=["first", "second", "third"]
|
||||
)
|
||||
|
||||
response = self.client.get(reverse('helpdesk:checklist_templates'))
|
||||
response = self.client.get(reverse("helpdesk:checklist_templates"))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html')
|
||||
self.assertContains(response, 'Test checklist template')
|
||||
self.assertContains(response, '3 tasks')
|
||||
self.assertTemplateUsed(response, "helpdesk/checklist_templates.html")
|
||||
self.assertContains(response, "Test checklist template")
|
||||
self.assertContains(response, "3 tasks")
|
||||
|
||||
def test_create_checklist_template(self):
|
||||
self.assertEqual(ChecklistTemplate.objects.count(), 0)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('helpdesk:checklist_templates'),
|
||||
reverse("helpdesk:checklist_templates"),
|
||||
data={
|
||||
'name': 'Test checklist template',
|
||||
'task_list': '["first", "second", "third"]'
|
||||
"name": "Test checklist template",
|
||||
"task_list": '["first", "second", "third"]',
|
||||
},
|
||||
follow=True
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html')
|
||||
self.assertTemplateUsed(response, "helpdesk/checklist_templates.html")
|
||||
|
||||
self.assertEqual(ChecklistTemplate.objects.count(), 1)
|
||||
checklist_template = ChecklistTemplate.objects.get()
|
||||
self.assertEqual(checklist_template.name, 'Test checklist template')
|
||||
self.assertEqual(checklist_template.task_list, ['first', 'second', 'third'])
|
||||
self.assertEqual(checklist_template.name, "Test checklist template")
|
||||
self.assertEqual(checklist_template.task_list, ["first", "second", "third"])
|
||||
|
||||
def test_edit_checklist_template(self):
|
||||
checklist_template = ChecklistTemplate.objects.create(
|
||||
name='Test checklist template',
|
||||
task_list=['first', 'second', 'third']
|
||||
name="Test checklist template", task_list=["first", "second", "third"]
|
||||
)
|
||||
self.assertEqual(ChecklistTemplate.objects.count(), 1)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('helpdesk:edit_checklist_template', kwargs={'checklist_template_id': checklist_template.id}),
|
||||
reverse(
|
||||
"helpdesk:edit_checklist_template",
|
||||
kwargs={"checklist_template_id": checklist_template.id},
|
||||
),
|
||||
data={
|
||||
'name': 'New checklist template',
|
||||
'task_list': '["new first", "second", "third", "last"]'
|
||||
"name": "New checklist template",
|
||||
"task_list": '["new first", "second", "third", "last"]',
|
||||
},
|
||||
follow=True
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html')
|
||||
self.assertTemplateUsed(response, "helpdesk/checklist_templates.html")
|
||||
|
||||
self.assertEqual(ChecklistTemplate.objects.count(), 1)
|
||||
checklist_template.refresh_from_db()
|
||||
self.assertEqual(checklist_template.name, 'New checklist template')
|
||||
self.assertEqual(checklist_template.task_list, ['new first', 'second', 'third', 'last'])
|
||||
self.assertEqual(checklist_template.name, "New checklist template")
|
||||
self.assertEqual(
|
||||
checklist_template.task_list, ["new first", "second", "third", "last"]
|
||||
)
|
||||
|
||||
def test_delete_checklist_template(self):
|
||||
checklist_template = ChecklistTemplate.objects.create(
|
||||
name='Test checklist template',
|
||||
task_list=['first', 'second', 'third']
|
||||
name="Test checklist template", task_list=["first", "second", "third"]
|
||||
)
|
||||
self.assertEqual(ChecklistTemplate.objects.count(), 1)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('helpdesk:delete_checklist_template', kwargs={'checklist_template_id': checklist_template.id}),
|
||||
follow=True
|
||||
reverse(
|
||||
"helpdesk:delete_checklist_template",
|
||||
kwargs={"checklist_template_id": checklist_template.id},
|
||||
),
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html')
|
||||
self.assertTemplateUsed(response, "helpdesk/checklist_templates.html")
|
||||
|
||||
self.assertEqual(ChecklistTemplate.objects.count(), 0)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -37,52 +37,52 @@ class KBTests(TestCase):
|
||||
self.user = get_staff_user()
|
||||
|
||||
def test_kb_index(self):
|
||||
response = self.client.get(reverse('helpdesk:kb_index'))
|
||||
self.assertContains(response, 'This is a test category')
|
||||
response = self.client.get(reverse("helpdesk:kb_index"))
|
||||
self.assertContains(response, "This is a test category")
|
||||
|
||||
def test_kb_category(self):
|
||||
response = self.client.get(
|
||||
reverse('helpdesk:kb_category', args=("test_cat", )))
|
||||
self.assertContains(response, 'This is a test category')
|
||||
self.assertContains(response, 'KBItem 1')
|
||||
self.assertContains(response, 'KBItem 2')
|
||||
self.assertContains(response, 'Create New Ticket Queue:')
|
||||
self.client.login(username=self.user.get_username(),
|
||||
password='password')
|
||||
response = self.client.get(
|
||||
reverse('helpdesk:kb_category', args=("test_cat", )))
|
||||
response = self.client.get(reverse("helpdesk:kb_category", args=("test_cat",)))
|
||||
self.assertContains(response, "This is a test category")
|
||||
self.assertContains(response, "KBItem 1")
|
||||
self.assertContains(response, "KBItem 2")
|
||||
self.assertContains(response, "Create New Ticket Queue:")
|
||||
self.client.login(username=self.user.get_username(), password="password")
|
||||
response = self.client.get(reverse("helpdesk:kb_category", args=("test_cat",)))
|
||||
self.assertContains(response, '<i class="fa fa-thumbs-up fa-lg"></i>')
|
||||
self.assertContains(response, '0 open tickets')
|
||||
self.assertContains(response, "0 open tickets")
|
||||
ticket = Ticket.objects.create(
|
||||
title="Test ticket",
|
||||
queue=self.queue,
|
||||
kbitem=self.kbitem1,
|
||||
)
|
||||
ticket.save()
|
||||
response = self.client.get(
|
||||
reverse('helpdesk:kb_category', args=("test_cat",)))
|
||||
self.assertContains(response, '1 open tickets')
|
||||
response = self.client.get(reverse("helpdesk:kb_category", args=("test_cat",)))
|
||||
self.assertContains(response, "1 open tickets")
|
||||
|
||||
def test_kb_vote(self):
|
||||
self.client.login(username=self.user.get_username(),
|
||||
password='password')
|
||||
self.client.login(username=self.user.get_username(), password="password")
|
||||
response = self.client.post(
|
||||
reverse('helpdesk:kb_vote', args=(self.kbitem1.pk, "up")), params={})
|
||||
cat_url = reverse('helpdesk:kb_category',
|
||||
args=("test_cat",)) + "?kbitem=1"
|
||||
reverse("helpdesk:kb_vote", args=(self.kbitem1.pk, "up")), params={}
|
||||
)
|
||||
cat_url = reverse("helpdesk:kb_category", args=("test_cat",)) + "?kbitem=1"
|
||||
self.assertRedirects(response, cat_url)
|
||||
response = self.client.get(cat_url)
|
||||
self.assertContains(response, '1 people found this answer useful of 1')
|
||||
self.assertContains(response, "1 people found this answer useful of 1")
|
||||
response = self.client.post(
|
||||
reverse('helpdesk:kb_vote', args=(self.kbitem1.pk, "down")), params={})
|
||||
reverse("helpdesk:kb_vote", args=(self.kbitem1.pk, "down")), params={}
|
||||
)
|
||||
self.assertRedirects(response, cat_url)
|
||||
response = self.client.get(cat_url)
|
||||
self.assertContains(response, '0 people found this answer useful of 1')
|
||||
self.assertContains(response, "0 people found this answer useful of 1")
|
||||
|
||||
def test_kb_category_iframe(self):
|
||||
cat_url = reverse('helpdesk:kb_category', args=(
|
||||
"test_cat",)) + "?kbitem=1&submitter_email=foo@bar.cz&title=lol&"
|
||||
cat_url = (
|
||||
reverse("helpdesk:kb_category", args=("test_cat",))
|
||||
+ "?kbitem=1&submitter_email=foo@bar.cz&title=lol&"
|
||||
)
|
||||
response = self.client.get(cat_url)
|
||||
# Assert that query params are passed on to ticket submit form
|
||||
self.assertContains(
|
||||
response, "'/tickets/submit/?queue=1&_readonly_fields_=queue&kbitem=1&submitter_email=foo%40bar.cz&title=lol")
|
||||
response,
|
||||
"'/tickets/submit/?queue=1&_readonly_fields_=queue&kbitem=1&submitter_email=foo%40bar.cz&title=lol",
|
||||
)
|
||||
|
@ -3,43 +3,42 @@ from django.urls import reverse
|
||||
|
||||
|
||||
class TestLoginRedirect(TestCase):
|
||||
|
||||
@override_settings(LOGIN_URL='/custom/login/')
|
||||
@override_settings(LOGIN_URL="/custom/login/")
|
||||
def test_custom_login_view_with_url(self):
|
||||
"""Test login redirect when LOGIN_URL is set to custom url"""
|
||||
response = self.client.get(reverse('helpdesk:login'))
|
||||
response = self.client.get(reverse("helpdesk:login"))
|
||||
# We expect that that helpdesk:home url is passed as next parameter in
|
||||
# the redirect url, so that the custom login can redirect the browser
|
||||
# back to helpdesk after the login.
|
||||
home_url = reverse('helpdesk:home')
|
||||
expected = '/custom/login/?next={}'.format(home_url)
|
||||
home_url = reverse("helpdesk:home")
|
||||
expected = "/custom/login/?next={}".format(home_url)
|
||||
self.assertRedirects(response, expected, fetch_redirect_response=False)
|
||||
|
||||
@override_settings(LOGIN_URL='/custom/login/')
|
||||
@override_settings(LOGIN_URL="/custom/login/")
|
||||
def test_custom_login_next_param(self):
|
||||
"""Test that the next url parameter is correctly relayed to custom login"""
|
||||
next_param = "/redirect/back"
|
||||
url = reverse('helpdesk:login') + "?next=" + next_param
|
||||
url = reverse("helpdesk:login") + "?next=" + next_param
|
||||
response = self.client.get(url)
|
||||
expected = '/custom/login/?next={}'.format(next_param)
|
||||
expected = "/custom/login/?next={}".format(next_param)
|
||||
self.assertRedirects(response, expected, fetch_redirect_response=False)
|
||||
|
||||
@override_settings(LOGIN_URL='helpdesk:login', SITE_ID=1)
|
||||
@override_settings(LOGIN_URL="helpdesk:login", SITE_ID=1)
|
||||
def test_default_login_view(self):
|
||||
"""Test that default login is used when LOGIN_URL is helpdesk:login"""
|
||||
response = self.client.get(reverse('helpdesk:login'))
|
||||
self.assertTemplateUsed(response, 'helpdesk/registration/login.html')
|
||||
response = self.client.get(reverse("helpdesk:login"))
|
||||
self.assertTemplateUsed(response, "helpdesk/registration/login.html")
|
||||
|
||||
@override_settings(LOGIN_URL=None, SITE_ID=1)
|
||||
def test_login_url_none(self):
|
||||
"""Test that default login is used when LOGIN_URL is None"""
|
||||
response = self.client.get(reverse('helpdesk:login'))
|
||||
self.assertTemplateUsed(response, 'helpdesk/registration/login.html')
|
||||
response = self.client.get(reverse("helpdesk:login"))
|
||||
self.assertTemplateUsed(response, "helpdesk/registration/login.html")
|
||||
|
||||
@override_settings(LOGIN_URL='admin:login', SITE_ID=1)
|
||||
@override_settings(LOGIN_URL="admin:login", SITE_ID=1)
|
||||
def test_custom_login_view_with_name(self):
|
||||
"""Test that LOGIN_URL can be a view name"""
|
||||
response = self.client.get(reverse('helpdesk:login'))
|
||||
home_url = reverse('helpdesk:home')
|
||||
expected = reverse('admin:login') + "?next=" + home_url
|
||||
response = self.client.get(reverse("helpdesk:login"))
|
||||
home_url = reverse("helpdesk:home")
|
||||
expected = reverse("admin:login") + "?next=" + home_url
|
||||
self.assertRedirects(response, expected)
|
||||
|
@ -1,10 +1,10 @@
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from helpdesk.models import get_markdown
|
||||
|
||||
|
||||
class MarkDown(SimpleTestCase):
|
||||
"""Test work Markdown functional"""
|
||||
|
||||
def test_markdown_html_tab(self):
|
||||
expected_value = "<p><div>test<div></p>"
|
||||
input_value = "<div>test<div>"
|
||||
@ -12,7 +12,7 @@ class MarkDown(SimpleTestCase):
|
||||
self.assertEqual(output_value, expected_value)
|
||||
|
||||
def test_markdown_nl2br(self):
|
||||
""" warning, after Line 1 - two withespace, esle did't work"""
|
||||
"""warning, after Line 1 - two withespace, esle did't work"""
|
||||
expected_value = "<p>Line 1<br />\n Line 2</p>"
|
||||
input_value = """Line 1
|
||||
Line 2"""
|
||||
|
@ -27,15 +27,15 @@ class KBDisabledTestCase(TestCase):
|
||||
"""Test proper rendering of navigation.html by accessing the dashboard"""
|
||||
from django.urls import NoReverseMatch
|
||||
|
||||
self.client.login(username=get_staff_user(
|
||||
).get_username(), password='password')
|
||||
self.assertRaises(NoReverseMatch, reverse, 'helpdesk:kb_index')
|
||||
self.client.login(username=get_staff_user().get_username(), password="password")
|
||||
self.assertRaises(NoReverseMatch, reverse, "helpdesk:kb_index")
|
||||
try:
|
||||
response = self.client.get(reverse('helpdesk:dashboard'))
|
||||
response = self.client.get(reverse("helpdesk:dashboard"))
|
||||
except NoReverseMatch as e:
|
||||
if 'helpdesk:kb_index' in e.message:
|
||||
if "helpdesk:kb_index" in e.message:
|
||||
self.fail(
|
||||
"Please verify any unchecked references to helpdesk_kb_index (start with navigation.html)")
|
||||
"Please verify any unchecked references to helpdesk_kb_index (start with navigation.html)"
|
||||
)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
@ -47,7 +47,9 @@ class StaffUserTestCaseMixin(object):
|
||||
|
||||
def setUp(self):
|
||||
self.original_setting = helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
||||
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = self.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
||||
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = (
|
||||
self.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
||||
)
|
||||
self.reload_views()
|
||||
|
||||
def tearDown(self):
|
||||
@ -56,16 +58,16 @@ class StaffUserTestCaseMixin(object):
|
||||
|
||||
def reload_views(self):
|
||||
try:
|
||||
reload(sys.modules['helpdesk.decorators'])
|
||||
reload(sys.modules['helpdesk.views.staff'])
|
||||
reload(sys.modules["helpdesk.decorators"])
|
||||
reload(sys.modules["helpdesk.views.staff"])
|
||||
reload_urlconf()
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def test_anonymous_user(self):
|
||||
"""Access to the dashboard always requires a login"""
|
||||
response = self.client.get(reverse('helpdesk:dashboard'), follow=True)
|
||||
self.assertTemplateUsed(response, 'helpdesk/registration/login.html')
|
||||
response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
|
||||
self.assertTemplateUsed(response, "helpdesk/registration/login.html")
|
||||
|
||||
|
||||
class NonStaffUsersAllowedTestCase(StaffUserTestCaseMixin, TestCase):
|
||||
@ -79,13 +81,16 @@ class NonStaffUsersAllowedTestCase(StaffUserTestCaseMixin, TestCase):
|
||||
from helpdesk.decorators import is_helpdesk_staff
|
||||
|
||||
user = User.objects.create_user(
|
||||
username='henry.wensleydale', password='gouda', email='wensleydale@example.com')
|
||||
username="henry.wensleydale",
|
||||
password="gouda",
|
||||
email="wensleydale@example.com",
|
||||
)
|
||||
|
||||
self.assertTrue(is_helpdesk_staff(user))
|
||||
|
||||
self.client.login(username=user.username, password='gouda')
|
||||
response = self.client.get(reverse('helpdesk:dashboard'), follow=True)
|
||||
self.assertTemplateUsed(response, 'helpdesk/dashboard.html')
|
||||
self.client.login(username=user.username, password="gouda")
|
||||
response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
|
||||
self.assertTemplateUsed(response, "helpdesk/dashboard.html")
|
||||
|
||||
|
||||
class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
|
||||
@ -96,7 +101,10 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
|
||||
super().setUp()
|
||||
self.non_staff_user_password = "gouda"
|
||||
self.non_staff_user = User.objects.create_user(
|
||||
username='henry.wensleydale', password=self.non_staff_user_password, email='wensleydale@example.com')
|
||||
username="henry.wensleydale",
|
||||
password=self.non_staff_user_password,
|
||||
email="wensleydale@example.com",
|
||||
)
|
||||
|
||||
def test_staff_user_detection(self):
|
||||
"""Staff and non-staff users are correctly identified"""
|
||||
@ -111,19 +119,18 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
|
||||
"""
|
||||
|
||||
user = get_staff_user()
|
||||
self.client.login(username=user.username, password='password')
|
||||
response = self.client.get(reverse('helpdesk:dashboard'), follow=True)
|
||||
self.assertTemplateUsed(response, 'helpdesk/dashboard.html')
|
||||
self.client.login(username=user.username, password="password")
|
||||
response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
|
||||
self.assertTemplateUsed(response, "helpdesk/dashboard.html")
|
||||
|
||||
def test_non_staff_cannot_access_dashboard(self):
|
||||
"""When HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False,
|
||||
non-staff users should not be able to access the dashboard.
|
||||
"""
|
||||
user = self.non_staff_user
|
||||
self.client.login(username=user.username,
|
||||
password=self.non_staff_user_password)
|
||||
response = self.client.get(reverse('helpdesk:dashboard'), follow=True)
|
||||
self.assertTemplateUsed(response, 'helpdesk/registration/login.html')
|
||||
self.client.login(username=user.username, password=self.non_staff_user_password)
|
||||
response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
|
||||
self.assertTemplateUsed(response, "helpdesk/registration/login.html")
|
||||
|
||||
def test_staff_rss(self):
|
||||
"""If HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False,
|
||||
@ -131,9 +138,8 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
|
||||
"""
|
||||
user = get_staff_user()
|
||||
self.client.login(username=user.username, password="password")
|
||||
response = self.client.get(
|
||||
reverse('helpdesk:rss_unassigned'), follow=True)
|
||||
self.assertContains(response, 'Unassigned Open and Reopened tickets')
|
||||
response = self.client.get(reverse("helpdesk:rss_unassigned"), follow=True)
|
||||
self.assertContains(response, "Unassigned Open and Reopened tickets")
|
||||
|
||||
@override_settings(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE=False)
|
||||
def test_non_staff_cannot_rss(self):
|
||||
@ -141,31 +147,32 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
|
||||
non-staff users should not be able to access rss feeds.
|
||||
"""
|
||||
user = self.non_staff_user
|
||||
self.client.login(username=user.username,
|
||||
password=self.non_staff_user_password)
|
||||
self.client.login(username=user.username, password=self.non_staff_user_password)
|
||||
queue = Queue.objects.create(
|
||||
title="Foo",
|
||||
slug="test_queue",
|
||||
)
|
||||
rss_urls = [
|
||||
reverse('helpdesk:rss_user', args=[user.username]),
|
||||
reverse('helpdesk:rss_user_queue', args=[
|
||||
user.username, 'test_queue']),
|
||||
reverse('helpdesk:rss_queue', args=['test_queue']),
|
||||
reverse('helpdesk:rss_unassigned'),
|
||||
reverse('helpdesk:rss_activity'),
|
||||
reverse("helpdesk:rss_user", args=[user.username]),
|
||||
reverse("helpdesk:rss_user_queue", args=[user.username, "test_queue"]),
|
||||
reverse("helpdesk:rss_queue", args=["test_queue"]),
|
||||
reverse("helpdesk:rss_unassigned"),
|
||||
reverse("helpdesk:rss_activity"),
|
||||
]
|
||||
for rss_url in rss_urls:
|
||||
response = self.client.get(rss_url, follow=True)
|
||||
self.assertTemplateUsed(
|
||||
response, 'helpdesk/registration/login.html')
|
||||
self.assertTemplateUsed(response, "helpdesk/registration/login.html")
|
||||
|
||||
|
||||
class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase):
|
||||
@staticmethod
|
||||
def custom_staff_filter(user):
|
||||
"""Arbitrary user validation function"""
|
||||
return user.is_authenticated and user.is_active and user.username.lower().endswith('wensleydale')
|
||||
return (
|
||||
user.is_authenticated
|
||||
and user.is_active
|
||||
and user.username.lower().endswith("wensleydale")
|
||||
)
|
||||
|
||||
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = custom_staff_filter
|
||||
|
||||
@ -176,25 +183,29 @@ class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase):
|
||||
from helpdesk.decorators import is_helpdesk_staff
|
||||
|
||||
user = User.objects.create_user(
|
||||
username='henry.wensleydale', password='gouda', email='wensleydale@example.com')
|
||||
username="henry.wensleydale",
|
||||
password="gouda",
|
||||
email="wensleydale@example.com",
|
||||
)
|
||||
|
||||
self.assertTrue(is_helpdesk_staff(user))
|
||||
|
||||
self.client.login(username=user.username, password='gouda')
|
||||
response = self.client.get(reverse('helpdesk:dashboard'), follow=True)
|
||||
self.assertTemplateUsed(response, 'helpdesk/dashboard.html')
|
||||
self.client.login(username=user.username, password="gouda")
|
||||
response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
|
||||
self.assertTemplateUsed(response, "helpdesk/dashboard.html")
|
||||
|
||||
def test_custom_staff_fail(self):
|
||||
from helpdesk.decorators import is_helpdesk_staff
|
||||
|
||||
user = User.objects.create_user(
|
||||
username='terry.milton', password='frog', email='milton@example.com')
|
||||
username="terry.milton", password="frog", email="milton@example.com"
|
||||
)
|
||||
|
||||
self.assertFalse(is_helpdesk_staff(user))
|
||||
|
||||
self.client.login(username=user.username, password='frog')
|
||||
response = self.client.get(reverse('helpdesk:dashboard'), follow=True)
|
||||
self.assertTemplateUsed(response, 'helpdesk/registration/login.html')
|
||||
self.client.login(username=user.username, password="frog")
|
||||
response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
|
||||
self.assertTemplateUsed(response, "helpdesk/registration/login.html")
|
||||
|
||||
|
||||
class HomePageAnonymousUserTestCase(TestCase):
|
||||
@ -206,14 +217,14 @@ class HomePageAnonymousUserTestCase(TestCase):
|
||||
|
||||
def test_homepage(self):
|
||||
helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = True
|
||||
response = self.client.get(reverse('helpdesk:home'))
|
||||
self.assertTemplateUsed('helpdesk/public_homepage.html')
|
||||
response = self.client.get(reverse("helpdesk:home"))
|
||||
self.assertTemplateUsed("helpdesk/public_homepage.html")
|
||||
|
||||
def test_redirect_to_login(self):
|
||||
"""Unauthenticated users are redirected to the login page if HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT is True"""
|
||||
helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = True
|
||||
response = self.client.get(reverse('helpdesk:home'))
|
||||
self.assertRedirects(response, reverse('helpdesk:login'))
|
||||
response = self.client.get(reverse("helpdesk:home"))
|
||||
self.assertRedirects(response, reverse("helpdesk:login"))
|
||||
|
||||
|
||||
class HomePageTestCase(TestCase):
|
||||
@ -221,17 +232,17 @@ class HomePageTestCase(TestCase):
|
||||
self.original_setting = helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
||||
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = False
|
||||
try:
|
||||
reload(sys.modules['helpdesk.views.public'])
|
||||
reload(sys.modules["helpdesk.views.public"])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = self.original_setting
|
||||
reload(sys.modules['helpdesk.views.public'])
|
||||
reload(sys.modules["helpdesk.views.public"])
|
||||
|
||||
def assertUserRedirectedToView(self, user, view_name):
|
||||
self.client.login(username=user.username, password='password')
|
||||
response = self.client.get(reverse('helpdesk:home'))
|
||||
self.client.login(username=user.username, password="password")
|
||||
response = self.client.get(reverse("helpdesk:home"))
|
||||
self.assertRedirects(response, reverse(view_name))
|
||||
self.client.logout()
|
||||
|
||||
@ -242,15 +253,16 @@ class HomePageTestCase(TestCase):
|
||||
# login_view_ticketlist is False...
|
||||
user.usersettings_helpdesk.login_view_ticketlist = False
|
||||
user.usersettings_helpdesk.save()
|
||||
self.assertUserRedirectedToView(user, 'helpdesk:dashboard')
|
||||
self.assertUserRedirectedToView(user, "helpdesk:dashboard")
|
||||
|
||||
def test_no_user_settings_redirect_to_dashboard(self):
|
||||
"""Authenticated users are redirected to the dashboard if user settings are missing"""
|
||||
from helpdesk.models import UserSettings
|
||||
|
||||
user = get_staff_user()
|
||||
|
||||
UserSettings.objects.filter(user=user).delete()
|
||||
self.assertUserRedirectedToView(user, 'helpdesk:dashboard')
|
||||
self.assertUserRedirectedToView(user, "helpdesk:dashboard")
|
||||
|
||||
def test_redirect_to_ticket_list(self):
|
||||
"""Authenticated users are redirected to the ticket list based on their user settings"""
|
||||
@ -258,7 +270,7 @@ class HomePageTestCase(TestCase):
|
||||
user.usersettings_helpdesk.login_view_ticketlist = True
|
||||
user.usersettings_helpdesk.save()
|
||||
|
||||
self.assertUserRedirectedToView(user, 'helpdesk:list')
|
||||
self.assertUserRedirectedToView(user, "helpdesk:list")
|
||||
|
||||
|
||||
class ReturnToTicketTestCase(TestCase):
|
||||
@ -268,13 +280,16 @@ class ReturnToTicketTestCase(TestCase):
|
||||
user = get_staff_user()
|
||||
ticket = create_ticket()
|
||||
response = return_to_ticket(user, helpdesk_settings, ticket)
|
||||
self.assertEqual(response['location'], ticket.get_absolute_url())
|
||||
self.assertEqual(response["location"], ticket.get_absolute_url())
|
||||
|
||||
def test_non_staff_user(self):
|
||||
from helpdesk.views.staff import return_to_ticket
|
||||
|
||||
user = User.objects.create_user(
|
||||
username='henry.wensleydale', password='gouda', email='wensleydale@example.com')
|
||||
username="henry.wensleydale",
|
||||
password="gouda",
|
||||
email="wensleydale@example.com",
|
||||
)
|
||||
ticket = create_ticket()
|
||||
response = return_to_ticket(user, helpdesk_settings, ticket)
|
||||
self.assertEqual(response['location'], ticket.ticket_url)
|
||||
self.assertEqual(response["location"], ticket.ticket_url)
|
||||
|
@ -10,7 +10,6 @@ from helpdesk.user import HelpdeskUser
|
||||
|
||||
|
||||
class PerQueueStaffMembershipTestCase(TestCase):
|
||||
|
||||
IDENTIFIERS = (1, 2)
|
||||
|
||||
def setUp(self):
|
||||
@ -19,31 +18,33 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
and user_2 with access to queue_2 containing 4 tickets
|
||||
and superuser who should be able to access both queues
|
||||
"""
|
||||
self.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION
|
||||
self.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = (
|
||||
settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION
|
||||
)
|
||||
settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = True
|
||||
self.client = Client()
|
||||
User = get_user_model()
|
||||
|
||||
self.superuser = User.objects.create(
|
||||
username='superuser',
|
||||
username="superuser",
|
||||
is_staff=True,
|
||||
is_superuser=True,
|
||||
)
|
||||
self.superuser.set_password('superuser')
|
||||
self.superuser.set_password("superuser")
|
||||
self.superuser.save()
|
||||
|
||||
self.identifier_users = {}
|
||||
|
||||
for identifier in self.IDENTIFIERS:
|
||||
queue = self.__dict__['queue_%d' % identifier] = Queue.objects.create(
|
||||
title='Queue %d' % identifier,
|
||||
slug='q%d' % identifier,
|
||||
queue = self.__dict__["queue_%d" % identifier] = Queue.objects.create(
|
||||
title="Queue %d" % identifier,
|
||||
slug="q%d" % identifier,
|
||||
)
|
||||
|
||||
user = self.__dict__['user_%d' % identifier] = User.objects.create(
|
||||
username='User_%d' % identifier,
|
||||
user = self.__dict__["user_%d" % identifier] = User.objects.create(
|
||||
username="User_%d" % identifier,
|
||||
is_staff=True,
|
||||
email="foo%s@example.com" % identifier
|
||||
email="foo%s@example.com" % identifier,
|
||||
)
|
||||
user.set_password(str(identifier))
|
||||
user.save()
|
||||
@ -55,13 +56,13 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
|
||||
for ticket_number in range(1, identifier + 1):
|
||||
Ticket.objects.create(
|
||||
title='Unassigned Ticket %d in Queue %d' % (
|
||||
ticket_number, identifier),
|
||||
title="Unassigned Ticket %d in Queue %d"
|
||||
% (ticket_number, identifier),
|
||||
queue=queue,
|
||||
)
|
||||
Ticket.objects.create(
|
||||
title='Ticket %d in Queue %d Assigned to User_%d' % (
|
||||
ticket_number, identifier, identifier),
|
||||
title="Ticket %d in Queue %d Assigned to User_%d"
|
||||
% (ticket_number, identifier, identifier),
|
||||
queue=queue,
|
||||
assigned_to=user,
|
||||
)
|
||||
@ -70,7 +71,9 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
"""
|
||||
Reset HELPDESK_ENABLE_PER_QUEUE_STAFF_MEMBERSHIP to original value
|
||||
"""
|
||||
settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = self.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION
|
||||
settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = (
|
||||
self.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION
|
||||
)
|
||||
|
||||
def test_dashboard_ticket_counts(self):
|
||||
"""
|
||||
@ -81,33 +84,32 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
|
||||
# Regular users
|
||||
for identifier in self.IDENTIFIERS:
|
||||
self.client.login(username='User_%d' %
|
||||
identifier, password=str(identifier))
|
||||
response = self.client.get(reverse('helpdesk:dashboard'))
|
||||
self.client.login(username="User_%d" % identifier, password=str(identifier))
|
||||
response = self.client.get(reverse("helpdesk:dashboard"))
|
||||
self.assertEqual(
|
||||
len(response.context['unassigned_tickets']),
|
||||
len(response.context["unassigned_tickets"]),
|
||||
identifier,
|
||||
'Unassigned tickets were not properly limited by queue membership'
|
||||
"Unassigned tickets were not properly limited by queue membership",
|
||||
)
|
||||
self.assertEqual(
|
||||
response.context['basic_ticket_stats']['open_ticket_stats'][0][1],
|
||||
response.context["basic_ticket_stats"]["open_ticket_stats"][0][1],
|
||||
identifier * 2,
|
||||
'Basic ticket stats were not properly limited by queue membership'
|
||||
"Basic ticket stats were not properly limited by queue membership",
|
||||
)
|
||||
|
||||
# Superuser
|
||||
self.client.login(username='superuser', password='superuser')
|
||||
response = self.client.get(reverse('helpdesk:dashboard'))
|
||||
self.client.login(username="superuser", password="superuser")
|
||||
response = self.client.get(reverse("helpdesk:dashboard"))
|
||||
self.assertEqual(
|
||||
len(response.context['unassigned_tickets']),
|
||||
len(response.context["unassigned_tickets"]),
|
||||
3,
|
||||
'Unassigned tickets were limited by queue membership for a superuser'
|
||||
"Unassigned tickets were limited by queue membership for a superuser",
|
||||
)
|
||||
self.assertEqual(
|
||||
response.context['basic_ticket_stats']['open_ticket_stats'][0][1] +
|
||||
response.context['basic_ticket_stats']['open_ticket_stats'][1][1],
|
||||
response.context["basic_ticket_stats"]["open_ticket_stats"][0][1]
|
||||
+ response.context["basic_ticket_stats"]["open_ticket_stats"][1][1],
|
||||
6,
|
||||
'Basic ticket stats were limited by queue membership for a superuser'
|
||||
"Basic ticket stats were limited by queue membership for a superuser",
|
||||
)
|
||||
|
||||
def test_report_ticket_counts(self):
|
||||
@ -119,44 +121,43 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
|
||||
# Regular users
|
||||
for identifier in self.IDENTIFIERS:
|
||||
self.client.login(username='User_%d' %
|
||||
identifier, password=str(identifier))
|
||||
response = self.client.get(reverse('helpdesk:report_index'))
|
||||
self.client.login(username="User_%d" % identifier, password=str(identifier))
|
||||
response = self.client.get(reverse("helpdesk:report_index"))
|
||||
self.assertEqual(
|
||||
len(response.context['dash_tickets']),
|
||||
len(response.context["dash_tickets"]),
|
||||
1,
|
||||
'The queues in dash_tickets were not properly limited by queue membership'
|
||||
"The queues in dash_tickets were not properly limited by queue membership",
|
||||
)
|
||||
self.assertEqual(
|
||||
response.context['dash_tickets'][0]['open'],
|
||||
response.context["dash_tickets"][0]["open"],
|
||||
identifier * 2,
|
||||
'The tickets in dash_tickets were not properly limited by queue membership'
|
||||
"The tickets in dash_tickets were not properly limited by queue membership",
|
||||
)
|
||||
self.assertEqual(
|
||||
response.context['basic_ticket_stats']['open_ticket_stats'][0][1],
|
||||
response.context["basic_ticket_stats"]["open_ticket_stats"][0][1],
|
||||
identifier * 2,
|
||||
'Basic ticket stats were not properly limited by queue membership'
|
||||
"Basic ticket stats were not properly limited by queue membership",
|
||||
)
|
||||
|
||||
# Superuser
|
||||
self.client.login(username='superuser', password='superuser')
|
||||
response = self.client.get(reverse('helpdesk:report_index'))
|
||||
self.client.login(username="superuser", password="superuser")
|
||||
response = self.client.get(reverse("helpdesk:report_index"))
|
||||
self.assertEqual(
|
||||
len(response.context['dash_tickets']),
|
||||
len(response.context["dash_tickets"]),
|
||||
2,
|
||||
'The queues in dash_tickets were limited by queue membership for a superuser'
|
||||
"The queues in dash_tickets were limited by queue membership for a superuser",
|
||||
)
|
||||
self.assertEqual(
|
||||
response.context['dash_tickets'][0]['open'] +
|
||||
response.context['dash_tickets'][1]['open'],
|
||||
response.context["dash_tickets"][0]["open"]
|
||||
+ response.context["dash_tickets"][1]["open"],
|
||||
6,
|
||||
'The tickets in dash_tickets were limited by queue membership for a superuser'
|
||||
"The tickets in dash_tickets were limited by queue membership for a superuser",
|
||||
)
|
||||
self.assertEqual(
|
||||
response.context['basic_ticket_stats']['open_ticket_stats'][0][1] +
|
||||
response.context['basic_ticket_stats']['open_ticket_stats'][1][1],
|
||||
response.context["basic_ticket_stats"]["open_ticket_stats"][0][1]
|
||||
+ response.context["basic_ticket_stats"]["open_ticket_stats"][1][1],
|
||||
6,
|
||||
'Basic ticket stats were limited by queue membership for a superuser'
|
||||
"Basic ticket stats were limited by queue membership for a superuser",
|
||||
)
|
||||
|
||||
def test_ticket_list_per_queue_user_restrictions(self):
|
||||
@ -167,36 +168,38 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
"""
|
||||
# Regular users
|
||||
for identifier in self.IDENTIFIERS:
|
||||
self.client.login(username='User_%d' %
|
||||
identifier, password=str(identifier))
|
||||
response = self.client.get(reverse('helpdesk:list'))
|
||||
tickets = __Query__(HelpdeskUser(
|
||||
self.identifier_users[identifier]), base64query=response.context['urlsafe_query']).get()
|
||||
self.client.login(username="User_%d" % identifier, password=str(identifier))
|
||||
response = self.client.get(reverse("helpdesk:list"))
|
||||
tickets = __Query__(
|
||||
HelpdeskUser(self.identifier_users[identifier]),
|
||||
base64query=response.context["urlsafe_query"],
|
||||
).get()
|
||||
self.assertEqual(
|
||||
len(tickets),
|
||||
identifier * 2,
|
||||
'Ticket list was not properly limited by queue membership'
|
||||
"Ticket list was not properly limited by queue membership",
|
||||
)
|
||||
self.assertEqual(
|
||||
len(response.context['queue_choices']),
|
||||
len(response.context["queue_choices"]),
|
||||
1,
|
||||
'Queue choices were not properly limited by queue membership'
|
||||
"Queue choices were not properly limited by queue membership",
|
||||
)
|
||||
self.assertEqual(
|
||||
response.context['queue_choices'][0],
|
||||
response.context["queue_choices"][0],
|
||||
Queue.objects.get(title="Queue %d" % identifier),
|
||||
'Queue choices were not properly limited by queue membership'
|
||||
"Queue choices were not properly limited by queue membership",
|
||||
)
|
||||
|
||||
# Superuser
|
||||
self.client.login(username='superuser', password='superuser')
|
||||
response = self.client.get(reverse('helpdesk:list'))
|
||||
tickets = __Query__(HelpdeskUser(self.superuser),
|
||||
base64query=response.context['urlsafe_query']).get()
|
||||
self.client.login(username="superuser", password="superuser")
|
||||
response = self.client.get(reverse("helpdesk:list"))
|
||||
tickets = __Query__(
|
||||
HelpdeskUser(self.superuser), base64query=response.context["urlsafe_query"]
|
||||
).get()
|
||||
self.assertEqual(
|
||||
len(tickets),
|
||||
6,
|
||||
'Ticket list was limited by queue membership for a superuser'
|
||||
"Ticket list was limited by queue membership for a superuser",
|
||||
)
|
||||
|
||||
def test_ticket_reports_per_queue_user_restrictions(self):
|
||||
@ -207,61 +210,60 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
"""
|
||||
# Regular users
|
||||
for identifier in self.IDENTIFIERS:
|
||||
self.client.login(username='User_%d' %
|
||||
identifier, password=str(identifier))
|
||||
self.client.login(username="User_%d" % identifier, password=str(identifier))
|
||||
response = self.client.get(
|
||||
reverse('helpdesk:run_report', kwargs={'report': 'userqueue'})
|
||||
reverse("helpdesk:run_report", kwargs={"report": "userqueue"})
|
||||
)
|
||||
# Only two columns of data should be present: ticket counts for
|
||||
# unassigned and this user only
|
||||
self.assertEqual(
|
||||
len(response.context['data']),
|
||||
len(response.context["data"]),
|
||||
2,
|
||||
'Queues in report were not properly limited by queue membership'
|
||||
"Queues in report were not properly limited by queue membership",
|
||||
)
|
||||
# Each user should see a total number of tickets equal to twice
|
||||
# their ID
|
||||
self.assertEqual(
|
||||
sum([sum(user_tickets[1:])
|
||||
for user_tickets in response.context['data']]),
|
||||
sum(
|
||||
[sum(user_tickets[1:]) for user_tickets in response.context["data"]]
|
||||
),
|
||||
identifier * 2,
|
||||
'Tickets in report were not properly limited by queue membership'
|
||||
"Tickets in report were not properly limited by queue membership",
|
||||
)
|
||||
# Each user should only be able to pick 1 queue
|
||||
self.assertEqual(
|
||||
len(response.context['headings']),
|
||||
len(response.context["headings"]),
|
||||
2,
|
||||
'Queue choices were not properly limited by queue membership'
|
||||
"Queue choices were not properly limited by queue membership",
|
||||
)
|
||||
# The queue each user can pick should be the queue named after
|
||||
# their ID
|
||||
self.assertEqual(
|
||||
response.context['headings'][1],
|
||||
response.context["headings"][1],
|
||||
"Queue %d" % identifier,
|
||||
'Queue choices were not properly limited by queue membership'
|
||||
"Queue choices were not properly limited by queue membership",
|
||||
)
|
||||
|
||||
# Superuser
|
||||
self.client.login(username='superuser', password='superuser')
|
||||
self.client.login(username="superuser", password="superuser")
|
||||
response = self.client.get(
|
||||
reverse('helpdesk:run_report', kwargs={'report': 'userqueue'})
|
||||
reverse("helpdesk:run_report", kwargs={"report": "userqueue"})
|
||||
)
|
||||
# Superuser should see ticket counts for all two queues, which includes
|
||||
# three columns: unassigned and both user 1 and user 2
|
||||
self.assertEqual(
|
||||
len(response.context['data'][0]),
|
||||
len(response.context["data"][0]),
|
||||
3,
|
||||
'Queues in report were improperly limited by queue membership for a superuser'
|
||||
"Queues in report were improperly limited by queue membership for a superuser",
|
||||
)
|
||||
# Superuser should see the total ticket count of three tickets
|
||||
self.assertEqual(
|
||||
sum([sum(user_tickets[1:])
|
||||
for user_tickets in response.context['data']]),
|
||||
sum([sum(user_tickets[1:]) for user_tickets in response.context["data"]]),
|
||||
6,
|
||||
'Tickets in report were improperly limited by queue membership for a superuser'
|
||||
"Tickets in report were improperly limited by queue membership for a superuser",
|
||||
)
|
||||
self.assertEqual(
|
||||
len(response.context['headings']),
|
||||
len(response.context["headings"]),
|
||||
3,
|
||||
'Queue choices were improperly limited by queue membership for a superuser'
|
||||
"Queue choices were improperly limited by queue membership for a superuser",
|
||||
)
|
||||
|
@ -16,39 +16,51 @@ class PublicActionsTestCase(TestCase):
|
||||
"""
|
||||
Create a queue & ticket we can use for later tests.
|
||||
"""
|
||||
self.queue = Queue.objects.create(title='Queue 1',
|
||||
slug='q',
|
||||
allow_public_submission=True,
|
||||
new_ticket_cc='new.public@example.com',
|
||||
updated_ticket_cc='update.public@example.com')
|
||||
self.ticket = Ticket.objects.create(title='Test Ticket',
|
||||
queue=self.queue,
|
||||
submitter_email='test.submitter@example.com',
|
||||
description='This is a test ticket.')
|
||||
self.queue = Queue.objects.create(
|
||||
title="Queue 1",
|
||||
slug="q",
|
||||
allow_public_submission=True,
|
||||
new_ticket_cc="new.public@example.com",
|
||||
updated_ticket_cc="update.public@example.com",
|
||||
)
|
||||
self.ticket = Ticket.objects.create(
|
||||
title="Test Ticket",
|
||||
queue=self.queue,
|
||||
submitter_email="test.submitter@example.com",
|
||||
description="This is a test ticket.",
|
||||
)
|
||||
|
||||
self.client = Client()
|
||||
|
||||
def test_public_view_ticket(self):
|
||||
# Without key, we get 403
|
||||
response = self.client.get('%s?ticket=%s&email=%s' % (
|
||||
reverse('helpdesk:public_view'),
|
||||
self.ticket.ticket_for_url,
|
||||
'test.submitter@example.com'))
|
||||
response = self.client.get(
|
||||
"%s?ticket=%s&email=%s"
|
||||
% (
|
||||
reverse("helpdesk:public_view"),
|
||||
self.ticket.ticket_for_url,
|
||||
"test.submitter@example.com",
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html')
|
||||
self.assertTemplateNotUsed(response, "helpdesk/public_view_form.html")
|
||||
# With a key it works
|
||||
response = self.client.get('%s?ticket=%s&email=%s&key=%s' % (
|
||||
reverse('helpdesk:public_view'),
|
||||
self.ticket.ticket_for_url,
|
||||
'test.submitter@example.com',
|
||||
self.ticket.secret_key))
|
||||
response = self.client.get(
|
||||
"%s?ticket=%s&email=%s&key=%s"
|
||||
% (
|
||||
reverse("helpdesk:public_view"),
|
||||
self.ticket.ticket_for_url,
|
||||
"test.submitter@example.com",
|
||||
self.ticket.secret_key,
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'helpdesk/public_view_ticket.html')
|
||||
self.assertTemplateUsed(response, "helpdesk/public_view_ticket.html")
|
||||
|
||||
def test_public_close(self):
|
||||
old_status = self.ticket.status
|
||||
old_resolution = self.ticket.resolution
|
||||
resolution_text = 'Resolved by test script'
|
||||
resolution_text = "Resolved by test script"
|
||||
|
||||
ticket = Ticket.objects.get(id=self.ticket.id)
|
||||
|
||||
@ -58,20 +70,23 @@ class PublicActionsTestCase(TestCase):
|
||||
|
||||
current_followups = ticket.followup_set.all().count()
|
||||
|
||||
response = self.client.get('%s?ticket=%s&email=%s&close&key=%s' % (
|
||||
reverse('helpdesk:public_view'),
|
||||
ticket.ticket_for_url,
|
||||
'test.submitter@example.com',
|
||||
ticket.secret_key))
|
||||
response = self.client.get(
|
||||
"%s?ticket=%s&email=%s&close&key=%s"
|
||||
% (
|
||||
reverse("helpdesk:public_view"),
|
||||
ticket.ticket_for_url,
|
||||
"test.submitter@example.com",
|
||||
ticket.secret_key,
|
||||
)
|
||||
)
|
||||
|
||||
ticket = Ticket.objects.get(id=self.ticket.id)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html')
|
||||
self.assertTemplateNotUsed(response, "helpdesk/public_view_form.html")
|
||||
self.assertEqual(ticket.status, Ticket.CLOSED_STATUS)
|
||||
self.assertEqual(ticket.resolution, resolution_text)
|
||||
self.assertEqual(current_followups + 1,
|
||||
ticket.followup_set.all().count())
|
||||
self.assertEqual(current_followups + 1, ticket.followup_set.all().count())
|
||||
|
||||
ticket.resolution = old_resolution
|
||||
ticket.status = old_status
|
||||
|
@ -47,25 +47,57 @@ class QueryTests(TestCase):
|
||||
"""Create a staff user and login"""
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create(
|
||||
username='User_1',
|
||||
username="User_1",
|
||||
is_staff=is_staff,
|
||||
)
|
||||
self.user.set_password('pass')
|
||||
self.user.set_password("pass")
|
||||
self.user.save()
|
||||
self.client.login(username='User_1', password='pass')
|
||||
self.client.login(username="User_1", password="pass")
|
||||
|
||||
def test_query_basic(self):
|
||||
self.loginUser()
|
||||
query = query_to_base64({})
|
||||
response = self.client.get(
|
||||
reverse('helpdesk:datatables_ticket_list', args=[query]))
|
||||
reverse("helpdesk:datatables_ticket_list", args=[query])
|
||||
)
|
||||
resp_json = response.json()
|
||||
self.assertEqual(
|
||||
resp_json,
|
||||
{
|
||||
"data":
|
||||
[{"ticket": "1 [test_queue-1]", "id": 1, "priority": 3, "title": "unassigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": ""},
|
||||
{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": resp_json["data"][1]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
|
||||
"data": [
|
||||
{
|
||||
"ticket": "1 [test_queue-1]",
|
||||
"id": 1,
|
||||
"priority": 3,
|
||||
"title": "unassigned to kbitem",
|
||||
"queue": {"title": "Test queue", "id": 1},
|
||||
"status": "Open",
|
||||
"created": resp_json["data"][0]["created"],
|
||||
"due_date": None,
|
||||
"assigned_to": "None",
|
||||
"submitter": None,
|
||||
"last_followup": None,
|
||||
"row_class": "",
|
||||
"time_spent": "",
|
||||
"kbitem": "",
|
||||
},
|
||||
{
|
||||
"ticket": "2 [test_queue-2]",
|
||||
"id": 2,
|
||||
"priority": 3,
|
||||
"title": "assigned to kbitem",
|
||||
"queue": {"title": "Test queue", "id": 1},
|
||||
"status": "Open",
|
||||
"created": resp_json["data"][1]["created"],
|
||||
"due_date": None,
|
||||
"assigned_to": "None",
|
||||
"submitter": None,
|
||||
"last_followup": None,
|
||||
"row_class": "",
|
||||
"time_spent": "",
|
||||
"kbitem": "KBItem 1",
|
||||
},
|
||||
],
|
||||
"recordsFiltered": 2,
|
||||
"recordsTotal": 2,
|
||||
"draw": 0,
|
||||
@ -74,18 +106,32 @@ class QueryTests(TestCase):
|
||||
|
||||
def test_query_by_kbitem(self):
|
||||
self.loginUser()
|
||||
query = query_to_base64(
|
||||
{'filtering': {'kbitem__in': [self.kbitem1.pk]}}
|
||||
)
|
||||
query = query_to_base64({"filtering": {"kbitem__in": [self.kbitem1.pk]}})
|
||||
response = self.client.get(
|
||||
reverse('helpdesk:datatables_ticket_list', args=[query]))
|
||||
reverse("helpdesk:datatables_ticket_list", args=[query])
|
||||
)
|
||||
resp_json = response.json()
|
||||
self.assertEqual(
|
||||
resp_json,
|
||||
{
|
||||
"data":
|
||||
[{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open",
|
||||
"created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
|
||||
"data": [
|
||||
{
|
||||
"ticket": "2 [test_queue-2]",
|
||||
"id": 2,
|
||||
"priority": 3,
|
||||
"title": "assigned to kbitem",
|
||||
"queue": {"title": "Test queue", "id": 1},
|
||||
"status": "Open",
|
||||
"created": resp_json["data"][0]["created"],
|
||||
"due_date": None,
|
||||
"assigned_to": "None",
|
||||
"submitter": None,
|
||||
"last_followup": None,
|
||||
"row_class": "",
|
||||
"time_spent": "",
|
||||
"kbitem": "KBItem 1",
|
||||
}
|
||||
],
|
||||
"recordsFiltered": 1,
|
||||
"recordsTotal": 1,
|
||||
"draw": 0,
|
||||
@ -94,18 +140,32 @@ class QueryTests(TestCase):
|
||||
|
||||
def test_query_by_no_kbitem(self):
|
||||
self.loginUser()
|
||||
query = query_to_base64(
|
||||
{'filtering_null': {'kbitem__isnull': True}}
|
||||
)
|
||||
query = query_to_base64({"filtering_null": {"kbitem__isnull": True}})
|
||||
response = self.client.get(
|
||||
reverse('helpdesk:datatables_ticket_list', args=[query]))
|
||||
reverse("helpdesk:datatables_ticket_list", args=[query])
|
||||
)
|
||||
resp_json = response.json()
|
||||
self.assertEqual(
|
||||
resp_json,
|
||||
{
|
||||
"data":
|
||||
[{"ticket": "1 [test_queue-1]", "id": 1, "priority": 3, "title": "unassigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open",
|
||||
"created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": ""}],
|
||||
"data": [
|
||||
{
|
||||
"ticket": "1 [test_queue-1]",
|
||||
"id": 1,
|
||||
"priority": 3,
|
||||
"title": "unassigned to kbitem",
|
||||
"queue": {"title": "Test queue", "id": 1},
|
||||
"status": "Open",
|
||||
"created": resp_json["data"][0]["created"],
|
||||
"due_date": None,
|
||||
"assigned_to": "None",
|
||||
"submitter": None,
|
||||
"last_followup": None,
|
||||
"row_class": "",
|
||||
"time_spent": "",
|
||||
"kbitem": "",
|
||||
}
|
||||
],
|
||||
"recordsFiltered": 1,
|
||||
"recordsTotal": 1,
|
||||
"draw": 0,
|
||||
|
@ -7,24 +7,25 @@ from helpdesk.tests.helpers import get_user
|
||||
|
||||
class TestSavingSharedQuery(TestCase):
|
||||
def setUp(self):
|
||||
q = Queue(title='Q1', slug='q1')
|
||||
q = Queue(title="Q1", slug="q1")
|
||||
q.save()
|
||||
self.q = q
|
||||
|
||||
def test_cansavequery(self):
|
||||
"""Can a query be saved"""
|
||||
url = reverse('helpdesk:savequery')
|
||||
self.client.login(username=get_user(is_staff=True).get_username(),
|
||||
password='password')
|
||||
url = reverse("helpdesk:savequery")
|
||||
self.client.login(
|
||||
username=get_user(is_staff=True).get_username(), password="password"
|
||||
)
|
||||
response = self.client.post(
|
||||
url,
|
||||
data={
|
||||
'title': 'ticket on my queue',
|
||||
'queue': self.q,
|
||||
'shared': 'on',
|
||||
'query_encoded':
|
||||
'KGRwMApWZmlsdGVyaW5nCnAxCihkcDIKVnN0YXR1c19faW4KcDMKKG'
|
||||
'xwNApJMQphSTIKYUkzCmFzc1Zzb3J0aW5nCnA1ClZjcmVhdGVkCnA2CnMu'
|
||||
})
|
||||
"title": "ticket on my queue",
|
||||
"queue": self.q,
|
||||
"shared": "on",
|
||||
"query_encoded": "KGRwMApWZmlsdGVyaW5nCnAxCihkcDIKVnN0YXR1c19faW4KcDMKKG"
|
||||
"xwNApJMQphSTIKYUkzCmFzc1Zzb3J0aW5nCnA1ClZjcmVhdGVkCnA2CnMu",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue('tickets/?saved_query=1' in response.url)
|
||||
self.assertTrue("tickets/?saved_query=1" in response.url)
|
||||
|
@ -17,29 +17,29 @@ except ImportError: # python 2
|
||||
|
||||
|
||||
class TicketActionsTestCase(TestCase):
|
||||
fixtures = ['emailtemplate.json']
|
||||
fixtures = ["emailtemplate.json"]
|
||||
|
||||
def setUp(self):
|
||||
self.queue_public = Queue.objects.create(
|
||||
title='Queue 1',
|
||||
slug='q1',
|
||||
title="Queue 1",
|
||||
slug="q1",
|
||||
allow_public_submission=True,
|
||||
new_ticket_cc='new.public@example.com',
|
||||
updated_ticket_cc='update.public@example.com'
|
||||
new_ticket_cc="new.public@example.com",
|
||||
updated_ticket_cc="update.public@example.com",
|
||||
)
|
||||
|
||||
self.queue_private = Queue.objects.create(
|
||||
title='Queue 2',
|
||||
slug='q2',
|
||||
title="Queue 2",
|
||||
slug="q2",
|
||||
allow_public_submission=False,
|
||||
new_ticket_cc='new.private@example.com',
|
||||
updated_ticket_cc='update.private@example.com'
|
||||
new_ticket_cc="new.private@example.com",
|
||||
updated_ticket_cc="update.private@example.com",
|
||||
)
|
||||
|
||||
self.ticket_data = {
|
||||
'queue': self.queue_public,
|
||||
'title': 'Test Ticket',
|
||||
'description': 'Some Test Ticket',
|
||||
"queue": self.queue_public,
|
||||
"title": "Test Ticket",
|
||||
"description": "Some Test Ticket",
|
||||
}
|
||||
|
||||
self.client = Client()
|
||||
@ -49,24 +49,22 @@ class TicketActionsTestCase(TestCase):
|
||||
"""Create a staff user and login"""
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create(
|
||||
username='User_1',
|
||||
username="User_1",
|
||||
is_staff=is_staff,
|
||||
)
|
||||
self.user.set_password('pass')
|
||||
self.user.set_password("pass")
|
||||
self.user.save()
|
||||
self.client.login(username='User_1', password='pass')
|
||||
self.client.login(username="User_1", password="pass")
|
||||
|
||||
def test_ticket_markdown(self):
|
||||
|
||||
ticket_data = {
|
||||
'queue': self.queue_public,
|
||||
'title': 'Test Ticket',
|
||||
'description': '*bold*',
|
||||
"queue": self.queue_public,
|
||||
"title": "Test Ticket",
|
||||
"description": "*bold*",
|
||||
}
|
||||
|
||||
ticket = Ticket.objects.create(**ticket_data)
|
||||
self.assertEqual(ticket.get_markdown(),
|
||||
"<p><em>bold</em></p>")
|
||||
self.assertEqual(ticket.get_markdown(), "<p><em>bold</em></p>")
|
||||
|
||||
def test_delete_ticket_staff(self):
|
||||
# make staff user
|
||||
@ -76,13 +74,14 @@ class TicketActionsTestCase(TestCase):
|
||||
ticket = Ticket.objects.create(**self.ticket_data)
|
||||
ticket_id = ticket.id
|
||||
|
||||
response = self.client.get(reverse('helpdesk:delete', kwargs={
|
||||
'ticket_id': ticket_id}), follow=True)
|
||||
self.assertContains(
|
||||
response, 'Are you sure you want to delete this ticket')
|
||||
response = self.client.get(
|
||||
reverse("helpdesk:delete", kwargs={"ticket_id": ticket_id}), follow=True
|
||||
)
|
||||
self.assertContains(response, "Are you sure you want to delete this ticket")
|
||||
|
||||
response = self.client.post(reverse('helpdesk:delete', kwargs={
|
||||
'ticket_id': ticket_id}), follow=True)
|
||||
response = self.client.post(
|
||||
reverse("helpdesk:delete", kwargs={"ticket_id": ticket_id}), follow=True
|
||||
)
|
||||
first_redirect = response.redirect_chain[0]
|
||||
first_redirect_url = first_redirect[0]
|
||||
|
||||
@ -90,7 +89,7 @@ class TicketActionsTestCase(TestCase):
|
||||
# Django 1.9 compatible way of testing this
|
||||
# https://docs.djangoproject.com/en/1.9/releases/1.9/#http-redirects-no-longer-forced-to-absolute-uris
|
||||
urlparts = urlparse(first_redirect_url)
|
||||
self.assertEqual(urlparts.path, reverse('helpdesk:home'))
|
||||
self.assertEqual(urlparts.path, reverse("helpdesk:home"))
|
||||
|
||||
# test ticket deleted
|
||||
with self.assertRaises(Ticket.DoesNotExist):
|
||||
@ -105,15 +104,15 @@ class TicketActionsTestCase(TestCase):
|
||||
# create second user
|
||||
User = get_user_model()
|
||||
self.user2 = User.objects.create(
|
||||
username='User_2',
|
||||
username="User_2",
|
||||
is_staff=True,
|
||||
)
|
||||
|
||||
initial_data = {
|
||||
'title': 'Private ticket test',
|
||||
'queue': self.queue_public,
|
||||
'assigned_to': self.user,
|
||||
'status': Ticket.OPEN_STATUS,
|
||||
"title": "Private ticket test",
|
||||
"queue": self.queue_public,
|
||||
"assigned_to": self.user,
|
||||
"status": Ticket.OPEN_STATUS,
|
||||
}
|
||||
|
||||
# create ticket
|
||||
@ -122,39 +121,45 @@ class TicketActionsTestCase(TestCase):
|
||||
|
||||
# assign new owner
|
||||
post_data = {
|
||||
'owner': self.user2.id,
|
||||
"owner": self.user2.id,
|
||||
}
|
||||
response = self.client.post(reverse('helpdesk:update', kwargs={
|
||||
'ticket_id': ticket_id}), post_data, follow=True)
|
||||
self.assertContains(response, 'Changed Owner from User_1 to User_2')
|
||||
response = self.client.post(
|
||||
reverse("helpdesk:update", kwargs={"ticket_id": ticket_id}),
|
||||
post_data,
|
||||
follow=True,
|
||||
)
|
||||
self.assertContains(response, "Changed Owner from User_1 to User_2")
|
||||
|
||||
# change status with users email assigned and submitter email assigned,
|
||||
# which triggers emails being sent
|
||||
ticket.assigned_to = self.user2
|
||||
ticket.submitter_email = 'submitter@test.com'
|
||||
ticket.submitter_email = "submitter@test.com"
|
||||
ticket.save()
|
||||
self.user2.email = 'user2@test.com'
|
||||
self.user2.email = "user2@test.com"
|
||||
self.user2.save()
|
||||
self.user.email = 'user1@test.com'
|
||||
self.user.email = "user1@test.com"
|
||||
self.user.save()
|
||||
post_data = {
|
||||
'new_status': Ticket.CLOSED_STATUS,
|
||||
'public': True
|
||||
}
|
||||
post_data = {"new_status": Ticket.CLOSED_STATUS, "public": True}
|
||||
|
||||
# do this also to a newly assigned user (different from logged in one)
|
||||
ticket.assigned_to = self.user
|
||||
response = self.client.post(reverse('helpdesk:update', kwargs={
|
||||
'ticket_id': ticket_id}), post_data, follow=True)
|
||||
self.assertContains(response, 'Changed Status from Open to Closed')
|
||||
response = self.client.post(
|
||||
reverse("helpdesk:update", kwargs={"ticket_id": ticket_id}),
|
||||
post_data,
|
||||
follow=True,
|
||||
)
|
||||
self.assertContains(response, "Changed Status from Open to Closed")
|
||||
post_data = {
|
||||
'new_status': Ticket.OPEN_STATUS,
|
||||
'owner': self.user2.id,
|
||||
'public': True
|
||||
"new_status": Ticket.OPEN_STATUS,
|
||||
"owner": self.user2.id,
|
||||
"public": True,
|
||||
}
|
||||
response = self.client.post(reverse('helpdesk:update', kwargs={
|
||||
'ticket_id': ticket_id}), post_data, follow=True)
|
||||
self.assertContains(response, 'Changed Status from Open to Closed')
|
||||
response = self.client.post(
|
||||
reverse("helpdesk:update", kwargs={"ticket_id": ticket_id}),
|
||||
post_data,
|
||||
follow=True,
|
||||
)
|
||||
self.assertContains(response, "Changed Status from Open to Closed")
|
||||
|
||||
def test_can_access_ticket(self):
|
||||
"""Tests whether non-staff but assigned user still counts as owner"""
|
||||
@ -165,24 +170,22 @@ class TicketActionsTestCase(TestCase):
|
||||
# create second user
|
||||
User = get_user_model()
|
||||
self.user2 = User.objects.create(
|
||||
username='User_2',
|
||||
username="User_2",
|
||||
is_staff=False,
|
||||
)
|
||||
|
||||
initial_data = {
|
||||
'title': 'Private ticket test',
|
||||
'queue': self.queue_private,
|
||||
'assigned_to': self.user,
|
||||
'status': Ticket.OPEN_STATUS,
|
||||
"title": "Private ticket test",
|
||||
"queue": self.queue_private,
|
||||
"assigned_to": self.user,
|
||||
"status": Ticket.OPEN_STATUS,
|
||||
}
|
||||
|
||||
# create ticket
|
||||
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = True
|
||||
ticket = Ticket.objects.create(**initial_data)
|
||||
self.assertEqual(HelpdeskUser(
|
||||
self.user).can_access_ticket(ticket), True)
|
||||
self.assertEqual(HelpdeskUser(
|
||||
self.user2).can_access_ticket(ticket), False)
|
||||
self.assertEqual(HelpdeskUser(self.user).can_access_ticket(ticket), True)
|
||||
self.assertEqual(HelpdeskUser(self.user2).can_access_ticket(ticket), False)
|
||||
|
||||
def test_num_to_link(self):
|
||||
"""Test that we are correctly expanding links to tickets from IDs"""
|
||||
@ -191,10 +194,10 @@ class TicketActionsTestCase(TestCase):
|
||||
self.loginUser()
|
||||
|
||||
initial_data = {
|
||||
'title': 'Some private ticket',
|
||||
'queue': self.queue_public,
|
||||
'assigned_to': self.user,
|
||||
'status': Ticket.OPEN_STATUS,
|
||||
"title": "Some private ticket",
|
||||
"queue": self.queue_public,
|
||||
"assigned_to": self.user,
|
||||
"status": Ticket.OPEN_STATUS,
|
||||
}
|
||||
|
||||
# create ticket
|
||||
@ -202,18 +205,23 @@ class TicketActionsTestCase(TestCase):
|
||||
ticket_id = ticket.id
|
||||
|
||||
# generate the URL text
|
||||
result = num_to_link('this is ticket#%s' % ticket_id)
|
||||
result = num_to_link("this is ticket#%s" % ticket_id)
|
||||
self.assertEqual(
|
||||
result, "this is ticket <a href='/tickets/%s/' class='ticket_link_status ticket_link_status_Open'>#%s</a>" % (ticket_id, ticket_id))
|
||||
result,
|
||||
"this is ticket <a href='/tickets/%s/' class='ticket_link_status ticket_link_status_Open'>#%s</a>"
|
||||
% (ticket_id, ticket_id),
|
||||
)
|
||||
|
||||
result2 = num_to_link(
|
||||
'whoa another ticket is here #%s huh' % ticket_id)
|
||||
result2 = num_to_link("whoa another ticket is here #%s huh" % ticket_id)
|
||||
self.assertEqual(
|
||||
result2, "whoa another ticket is here <a href='/tickets/%s/' class='ticket_link_status ticket_link_status_Open'>#%s</a> huh" % (ticket_id, ticket_id))
|
||||
result2,
|
||||
"whoa another ticket is here <a href='/tickets/%s/' class='ticket_link_status ticket_link_status_Open'>#%s</a> huh"
|
||||
% (ticket_id, ticket_id),
|
||||
)
|
||||
|
||||
def test_create_ticket_getform(self):
|
||||
self.loginUser()
|
||||
response = self.client.get(reverse('helpdesk:submit'), follow=True)
|
||||
response = self.client.get(reverse("helpdesk:submit"), follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# TODO this needs to be checked further
|
||||
@ -224,61 +232,62 @@ class TicketActionsTestCase(TestCase):
|
||||
# Create two tickets
|
||||
ticket_1 = Ticket.objects.create(
|
||||
queue=self.queue_public,
|
||||
title='Ticket 1',
|
||||
description='Description from ticket 1',
|
||||
submitter_email='user1@mail.com',
|
||||
title="Ticket 1",
|
||||
description="Description from ticket 1",
|
||||
submitter_email="user1@mail.com",
|
||||
status=Ticket.RESOLVED_STATUS,
|
||||
resolution='Awesome resolution for ticket 1'
|
||||
resolution="Awesome resolution for ticket 1",
|
||||
)
|
||||
ticket_1_follow_up = ticket_1.followup_set.create(
|
||||
title='Ticket 1 creation')
|
||||
ticket_1_follow_up = ticket_1.followup_set.create(title="Ticket 1 creation")
|
||||
ticket_1_cc = ticket_1.ticketcc_set.create(user=self.user)
|
||||
ticket_1_created = ticket_1.created
|
||||
due_date = timezone.now()
|
||||
ticket_2 = Ticket.objects.create(
|
||||
queue=self.queue_public,
|
||||
title='Ticket 2',
|
||||
description='Description from ticket 2',
|
||||
submitter_email='user2@mail.com',
|
||||
title="Ticket 2",
|
||||
description="Description from ticket 2",
|
||||
submitter_email="user2@mail.com",
|
||||
due_date=due_date,
|
||||
assigned_to=self.user
|
||||
assigned_to=self.user,
|
||||
)
|
||||
ticket_2_follow_up = ticket_1.followup_set.create(
|
||||
title='Ticket 2 creation')
|
||||
ticket_2_cc = ticket_2.ticketcc_set.create(email='random@mail.com')
|
||||
ticket_2_follow_up = ticket_1.followup_set.create(title="Ticket 2 creation")
|
||||
ticket_2_cc = ticket_2.ticketcc_set.create(email="random@mail.com")
|
||||
|
||||
# Create custom fields and set values for tickets
|
||||
custom_field_1 = CustomField.objects.create(
|
||||
name='test',
|
||||
label='Test',
|
||||
data_type='varchar',
|
||||
name="test",
|
||||
label="Test",
|
||||
data_type="varchar",
|
||||
)
|
||||
ticket_1_field_1 = 'This is for the test field'
|
||||
ticket_1_field_1 = "This is for the test field"
|
||||
ticket_1.ticketcustomfieldvalue_set.create(
|
||||
field=custom_field_1, value=ticket_1_field_1)
|
||||
ticket_2_field_1 = 'Another test text'
|
||||
ticket_2.ticketcustomfieldvalue_set.create(
|
||||
field=custom_field_1, value=ticket_2_field_1)
|
||||
custom_field_2 = CustomField.objects.create(
|
||||
name='number',
|
||||
label='Number',
|
||||
data_type='integer',
|
||||
field=custom_field_1, value=ticket_1_field_1
|
||||
)
|
||||
ticket_2_field_2 = '444'
|
||||
ticket_2_field_1 = "Another test text"
|
||||
ticket_2.ticketcustomfieldvalue_set.create(
|
||||
field=custom_field_2, value=ticket_2_field_2)
|
||||
field=custom_field_1, value=ticket_2_field_1
|
||||
)
|
||||
custom_field_2 = CustomField.objects.create(
|
||||
name="number",
|
||||
label="Number",
|
||||
data_type="integer",
|
||||
)
|
||||
ticket_2_field_2 = "444"
|
||||
ticket_2.ticketcustomfieldvalue_set.create(
|
||||
field=custom_field_2, value=ticket_2_field_2
|
||||
)
|
||||
|
||||
# Check that it correctly redirects to the intermediate page
|
||||
response = self.client.post(
|
||||
reverse('helpdesk:mass_update'),
|
||||
data={
|
||||
'ticket_id': [str(ticket_1.id), str(ticket_2.id)],
|
||||
'action': 'merge'
|
||||
},
|
||||
follow=True
|
||||
reverse("helpdesk:mass_update"),
|
||||
data={"ticket_id": [str(ticket_1.id), str(ticket_2.id)], "action": "merge"},
|
||||
follow=True,
|
||||
)
|
||||
redirect_url = "%s?tickets=%s&tickets=%s" % (
|
||||
reverse("helpdesk:merge_tickets"),
|
||||
ticket_1.id,
|
||||
ticket_2.id,
|
||||
)
|
||||
redirect_url = '%s?tickets=%s&tickets=%s' % (
|
||||
reverse('helpdesk:merge_tickets'), ticket_1.id, ticket_2.id)
|
||||
self.assertRedirects(response, redirect_url)
|
||||
self.assertContains(response, ticket_1.description)
|
||||
self.assertContains(response, ticket_1.resolution)
|
||||
@ -293,16 +302,16 @@ class TicketActionsTestCase(TestCase):
|
||||
response = self.client.post(
|
||||
redirect_url,
|
||||
data={
|
||||
'chosen_ticket': str(ticket_1.id),
|
||||
'due_date': str(ticket_2.id),
|
||||
'status': str(ticket_1.id),
|
||||
'submitter_email': str(ticket_2.id),
|
||||
'description': str(ticket_2.id),
|
||||
'assigned_to': str(ticket_2.id),
|
||||
"chosen_ticket": str(ticket_1.id),
|
||||
"due_date": str(ticket_2.id),
|
||||
"status": str(ticket_1.id),
|
||||
"submitter_email": str(ticket_2.id),
|
||||
"description": str(ticket_2.id),
|
||||
"assigned_to": str(ticket_2.id),
|
||||
custom_field_1.name: str(ticket_1.id),
|
||||
custom_field_2.name: str(ticket_2.id),
|
||||
},
|
||||
follow=True
|
||||
follow=True,
|
||||
)
|
||||
self.assertRedirects(response, ticket_1.get_absolute_url())
|
||||
ticket_2.refresh_from_db()
|
||||
@ -316,14 +325,18 @@ class TicketActionsTestCase(TestCase):
|
||||
self.assertEqual(ticket_1.submitter_email, ticket_2.submitter_email)
|
||||
self.assertEqual(ticket_1.description, ticket_2.description)
|
||||
self.assertEqual(ticket_1.assigned_to, ticket_2.assigned_to)
|
||||
self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(
|
||||
field=custom_field_1).value, ticket_1_field_1)
|
||||
self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(
|
||||
field=custom_field_2).value, ticket_2_field_2)
|
||||
self.assertEqual(list(ticket_1.followup_set.all()), [
|
||||
ticket_1_follow_up, ticket_2_follow_up])
|
||||
self.assertEqual(list(ticket_1.ticketcc_set.all()),
|
||||
[ticket_1_cc, ticket_2_cc])
|
||||
self.assertEqual(
|
||||
ticket_1.ticketcustomfieldvalue_set.get(field=custom_field_1).value,
|
||||
ticket_1_field_1,
|
||||
)
|
||||
self.assertEqual(
|
||||
ticket_1.ticketcustomfieldvalue_set.get(field=custom_field_2).value,
|
||||
ticket_2_field_2,
|
||||
)
|
||||
self.assertEqual(
|
||||
list(ticket_1.followup_set.all()), [ticket_1_follow_up, ticket_2_follow_up]
|
||||
)
|
||||
self.assertEqual(list(ticket_1.ticketcc_set.all()), [ticket_1_cc, ticket_2_cc])
|
||||
|
||||
def test_update_ticket_queue(self):
|
||||
"""Tests whether user can change the queue in the Respond to this ticket section."""
|
||||
@ -333,10 +346,10 @@ class TicketActionsTestCase(TestCase):
|
||||
|
||||
# create ticket
|
||||
initial_data = {
|
||||
'title': 'Queue change ticket test',
|
||||
'queue': self.queue_public,
|
||||
'assigned_to': self.user,
|
||||
'status': Ticket.OPEN_STATUS,
|
||||
"title": "Queue change ticket test",
|
||||
"queue": self.queue_public,
|
||||
"assigned_to": self.user,
|
||||
"status": Ticket.OPEN_STATUS,
|
||||
}
|
||||
ticket = Ticket.objects.create(**initial_data)
|
||||
ticket_id = ticket.id
|
||||
@ -346,24 +359,24 @@ class TicketActionsTestCase(TestCase):
|
||||
|
||||
# POST first follow-up with new queue
|
||||
new_queue = Queue.objects.create(
|
||||
title='New Queue',
|
||||
slug='newqueue',
|
||||
title="New Queue",
|
||||
slug="newqueue",
|
||||
)
|
||||
post_data = {
|
||||
'comment': 'first follow-up in new queue',
|
||||
'queue': str(new_queue.id),
|
||||
"comment": "first follow-up in new queue",
|
||||
"queue": str(new_queue.id),
|
||||
}
|
||||
response = self.client.post(reverse('helpdesk:update',
|
||||
kwargs={'ticket_id': ticket_id}),
|
||||
post_data)
|
||||
response = self.client.post(
|
||||
reverse("helpdesk:update", kwargs={"ticket_id": ticket_id}), post_data
|
||||
)
|
||||
|
||||
# queue was correctly modified
|
||||
ticket.refresh_from_db()
|
||||
self.assertEqual(ticket.queue, new_queue)
|
||||
|
||||
# ticket change was saved
|
||||
latest_fup = ticket.followup_set.latest('date')
|
||||
latest_ticketchange = latest_fup.ticketchange_set.latest('id')
|
||||
self.assertEqual(latest_ticketchange.field, _('Queue'))
|
||||
latest_fup = ticket.followup_set.latest("date")
|
||||
latest_ticketchange = latest_fup.ticketchange_set.latest("id")
|
||||
self.assertEqual(latest_ticketchange.field, _("Queue"))
|
||||
self.assertEqual(int(latest_ticketchange.old_value), self.queue_public.id)
|
||||
self.assertEqual(int(latest_ticketchange.new_value), new_queue.id)
|
||||
self.assertEqual(int(latest_ticketchange.new_value), new_queue.id)
|
||||
|
@ -9,14 +9,12 @@ from helpdesk.models import Queue, Ticket
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@override_settings(
|
||||
HELPDESK_VIEW_A_TICKET_PUBLIC=True
|
||||
)
|
||||
@override_settings(HELPDESK_VIEW_A_TICKET_PUBLIC=True)
|
||||
class TestTicketLookupPublicEnabled(TestCase):
|
||||
def setUp(self):
|
||||
q = Queue(title='Q1', slug='q1')
|
||||
q = Queue(title="Q1", slug="q1")
|
||||
q.save()
|
||||
t = Ticket(title='Test Ticket', submitter_email='test@domain.com')
|
||||
t = Ticket(title="Test Ticket", submitter_email="test@domain.com")
|
||||
t.queue = q
|
||||
t.save()
|
||||
self.ticket = t
|
||||
@ -33,20 +31,26 @@ class TestTicketLookupPublicEnabled(TestCase):
|
||||
# we will exercise 'reverse' to lookup/build the URL
|
||||
# from the ticket info we have
|
||||
# http://example.com/helpdesk/view/?ticket=q1-1&email=None
|
||||
response = self.client.get(reverse('helpdesk:public_view'),
|
||||
{'ticket': self.ticket.ticket_for_url,
|
||||
'email': self.ticket.submitter_email})
|
||||
response = self.client.get(
|
||||
reverse("helpdesk:public_view"),
|
||||
{
|
||||
"ticket": self.ticket.ticket_for_url,
|
||||
"email": self.ticket.submitter_email,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_ticket_with_changed_queue(self):
|
||||
# Make a ticket (already done in setup() )
|
||||
# Now make another queue
|
||||
q2 = Queue(title='Q2', slug='q2')
|
||||
q2 = Queue(title="Q2", slug="q2")
|
||||
q2.save()
|
||||
# grab the URL / params which would have been emailed out to submitter.
|
||||
url = reverse('helpdesk:public_view')
|
||||
params = {'ticket': self.ticket.ticket_for_url,
|
||||
'email': self.ticket.submitter_email}
|
||||
url = reverse("helpdesk:public_view")
|
||||
params = {
|
||||
"ticket": self.ticket.ticket_for_url,
|
||||
"email": self.ticket.submitter_email,
|
||||
}
|
||||
# Pickup the ticket created in setup() and change its queue
|
||||
self.ticket.queue = q2
|
||||
self.ticket.save()
|
||||
@ -56,36 +60,34 @@ class TestTicketLookupPublicEnabled(TestCase):
|
||||
self.assertNotContains(response, "Invalid ticket ID")
|
||||
|
||||
def test_add_email_to_ticketcc_if_not_in(self):
|
||||
staff_email = 'staff@mail.com'
|
||||
staff_email = "staff@mail.com"
|
||||
staff_user = User.objects.create(
|
||||
username='staff', email=staff_email, is_staff=True)
|
||||
username="staff", email=staff_email, is_staff=True
|
||||
)
|
||||
self.ticket.assigned_to = staff_user
|
||||
self.ticket.save()
|
||||
email_1 = 'user1@mail.com'
|
||||
email_1 = "user1@mail.com"
|
||||
ticketcc_1 = self.ticket.ticketcc_set.create(email=email_1)
|
||||
|
||||
# Add new email to CC
|
||||
email_2 = 'user2@mail.com'
|
||||
email_2 = "user2@mail.com"
|
||||
ticketcc_2 = self.ticket.add_email_to_ticketcc_if_not_in(email=email_2)
|
||||
self.assertEqual(list(self.ticket.ticketcc_set.all()),
|
||||
[ticketcc_1, ticketcc_2])
|
||||
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
|
||||
|
||||
# Add existing email, doesn't change anything
|
||||
self.ticket.add_email_to_ticketcc_if_not_in(email=email_1)
|
||||
self.assertEqual(list(self.ticket.ticketcc_set.all()),
|
||||
[ticketcc_1, ticketcc_2])
|
||||
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
|
||||
|
||||
# Add mail from assigned user, doesn't change anything
|
||||
self.ticket.add_email_to_ticketcc_if_not_in(email=staff_email)
|
||||
self.assertEqual(list(self.ticket.ticketcc_set.all()),
|
||||
[ticketcc_1, ticketcc_2])
|
||||
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
|
||||
self.ticket.add_email_to_ticketcc_if_not_in(user=staff_user)
|
||||
self.assertEqual(list(self.ticket.ticketcc_set.all()),
|
||||
[ticketcc_1, ticketcc_2])
|
||||
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
|
||||
|
||||
# Move a ticketCC from ticket 1 to ticket 2
|
||||
ticket_2 = Ticket.objects.create(
|
||||
queue=self.ticket.queue, title='Ticket 2', submitter_email=email_2)
|
||||
queue=self.ticket.queue, title="Ticket 2", submitter_email=email_2
|
||||
)
|
||||
self.assertEqual(ticket_2.ticketcc_set.count(), 0)
|
||||
ticket_2.add_email_to_ticketcc_if_not_in(ticketcc=ticketcc_1)
|
||||
self.assertEqual(ticketcc_1.ticket, ticket_2)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,3 @@
|
||||
|
||||
import datetime
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.contrib.auth.models import User
|
||||
@ -7,19 +6,19 @@ from django.test.client import Client
|
||||
from helpdesk.models import FollowUp, Queue, Ticket
|
||||
import uuid
|
||||
|
||||
class TimeSpentTestCase(TestCase):
|
||||
|
||||
class TimeSpentTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.queue_public = Queue.objects.create(
|
||||
title='Queue 1',
|
||||
slug='q1',
|
||||
title="Queue 1",
|
||||
slug="q1",
|
||||
allow_public_submission=True,
|
||||
dedicated_time=datetime.timedelta(minutes=60)
|
||||
dedicated_time=datetime.timedelta(minutes=60),
|
||||
)
|
||||
|
||||
self.ticket_data = {
|
||||
'title': 'Test Ticket',
|
||||
'description': 'Some Test Ticket',
|
||||
"title": "Test Ticket",
|
||||
"description": "Some Test Ticket",
|
||||
}
|
||||
|
||||
ticket_data = dict(queue=self.queue_public, **self.ticket_data)
|
||||
@ -28,12 +27,12 @@ class TimeSpentTestCase(TestCase):
|
||||
self.client = Client()
|
||||
|
||||
user1_kwargs = {
|
||||
'username': 'staff',
|
||||
'email': 'staff@example.com',
|
||||
'password': make_password('Test1234'),
|
||||
'is_staff': True,
|
||||
'is_superuser': False,
|
||||
'is_active': True
|
||||
"username": "staff",
|
||||
"email": "staff@example.com",
|
||||
"password": make_password("Test1234"),
|
||||
"is_staff": True,
|
||||
"is_superuser": False,
|
||||
"is_active": True,
|
||||
}
|
||||
self.user = User.objects.create(**user1_kwargs)
|
||||
|
||||
@ -50,7 +49,7 @@ class TimeSpentTestCase(TestCase):
|
||||
user=self.user,
|
||||
new_status=1,
|
||||
message_id=message_id,
|
||||
time_spent=datetime.timedelta(minutes=30)
|
||||
time_spent=datetime.timedelta(minutes=30),
|
||||
)
|
||||
|
||||
followup.save()
|
||||
@ -59,5 +58,6 @@ class TimeSpentTestCase(TestCase):
|
||||
self.assertEqual(self.ticket.time_spent.seconds, 1800)
|
||||
self.assertEqual(self.queue_public.time_spent.seconds, 1800)
|
||||
self.assertTrue(
|
||||
self.queue_public.dedicated_time.seconds > self.queue_public.time_spent.seconds
|
||||
self.queue_public.dedicated_time.seconds
|
||||
> self.queue_public.time_spent.seconds
|
||||
)
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.contrib.auth.models import User
|
||||
@ -13,43 +12,42 @@ import uuid
|
||||
|
||||
@override_settings(USE_TZ=True)
|
||||
class TimeSpentAutoTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Creates a queue, ticket and user."""
|
||||
self.queue_public = Queue.objects.create(
|
||||
title='Queue 1',
|
||||
slug='q1',
|
||||
title="Queue 1",
|
||||
slug="q1",
|
||||
allow_public_submission=True,
|
||||
dedicated_time=timedelta(minutes=60)
|
||||
dedicated_time=timedelta(minutes=60),
|
||||
)
|
||||
|
||||
self.ticket_data = dict(queue=self.queue_public,
|
||||
title='test ticket',
|
||||
description='test ticket description')
|
||||
self.ticket_data = dict(
|
||||
queue=self.queue_public,
|
||||
title="test ticket",
|
||||
description="test ticket description",
|
||||
)
|
||||
|
||||
self.user = User.objects.create(
|
||||
username='staff',
|
||||
email='staff@example.com',
|
||||
password=make_password('Test1234'),
|
||||
username="staff",
|
||||
email="staff@example.com",
|
||||
password=make_password("Test1234"),
|
||||
is_staff=True,
|
||||
is_superuser=False,
|
||||
is_active=True
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
self.client = Client()
|
||||
|
||||
|
||||
def loginUser(self, is_staff=True):
|
||||
"""Create a staff user and login"""
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create(
|
||||
username='User_1',
|
||||
username="User_1",
|
||||
is_staff=is_staff,
|
||||
)
|
||||
self.user.set_password('pass')
|
||||
self.user.set_password("pass")
|
||||
self.user.save()
|
||||
self.client.login(username='User_1', password='pass')
|
||||
|
||||
self.client.login(username="User_1", password="pass")
|
||||
|
||||
def test_add_two_followups_time_spent_auto(self):
|
||||
"""Tests automatic time_spent calculation."""
|
||||
@ -59,15 +57,39 @@ class TimeSpentAutoTestCase(TestCase):
|
||||
# ticket creation date, follow-up creation date, assertion value
|
||||
TEST_VALUES = (
|
||||
# friday
|
||||
('2024-03-01T00:00:00+00:00', '2024-03-01T09:30:10+00:00', timedelta(hours=9, minutes=30, seconds=10)),
|
||||
('2024-03-01T00:00:00+00:00', '2024-03-01T23:59:58+00:00', timedelta(hours=23, minutes=59, seconds=58)),
|
||||
('2024-03-01T00:00:00+00:00', '2024-03-01T23:59:59+00:00', timedelta(hours=23, minutes=59, seconds=59)),
|
||||
('2024-03-01T00:00:00+00:00', '2024-03-02T00:00:00+00:00', timedelta(hours=24)),
|
||||
('2024-03-01T00:00:00+00:00', '2024-03-02T09:00:00+00:00', timedelta(hours=33)),
|
||||
('2024-03-01T00:00:00+00:00', '2024-03-03T00:00:00+00:00', timedelta(hours=48)),
|
||||
(
|
||||
"2024-03-01T00:00:00+00:00",
|
||||
"2024-03-01T09:30:10+00:00",
|
||||
timedelta(hours=9, minutes=30, seconds=10),
|
||||
),
|
||||
(
|
||||
"2024-03-01T00:00:00+00:00",
|
||||
"2024-03-01T23:59:58+00:00",
|
||||
timedelta(hours=23, minutes=59, seconds=58),
|
||||
),
|
||||
(
|
||||
"2024-03-01T00:00:00+00:00",
|
||||
"2024-03-01T23:59:59+00:00",
|
||||
timedelta(hours=23, minutes=59, seconds=59),
|
||||
),
|
||||
(
|
||||
"2024-03-01T00:00:00+00:00",
|
||||
"2024-03-02T00:00:00+00:00",
|
||||
timedelta(hours=24),
|
||||
),
|
||||
(
|
||||
"2024-03-01T00:00:00+00:00",
|
||||
"2024-03-02T09:00:00+00:00",
|
||||
timedelta(hours=33),
|
||||
),
|
||||
(
|
||||
"2024-03-01T00:00:00+00:00",
|
||||
"2024-03-03T00:00:00+00:00",
|
||||
timedelta(hours=48),
|
||||
),
|
||||
)
|
||||
|
||||
for (ticket_time, fup_time, assertion_delta) in TEST_VALUES:
|
||||
for ticket_time, fup_time, assertion_delta in TEST_VALUES:
|
||||
# create and setup test ticket time
|
||||
ticket = Ticket.objects.create(**self.ticket_data)
|
||||
ticket_time_p = datetime.strptime(ticket_time, "%Y-%m-%dT%H:%M:%S%z")
|
||||
@ -85,15 +107,24 @@ class TimeSpentAutoTestCase(TestCase):
|
||||
user=self.user,
|
||||
new_status=1,
|
||||
message_id=uuid.uuid4().hex,
|
||||
time_spent=None
|
||||
time_spent=None,
|
||||
)
|
||||
|
||||
self.assertEqual(followup1.time_spent.total_seconds(), assertion_delta.total_seconds())
|
||||
self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds())
|
||||
self.assertEqual(
|
||||
followup1.time_spent.total_seconds(), assertion_delta.total_seconds()
|
||||
)
|
||||
self.assertEqual(
|
||||
ticket.time_spent.total_seconds(), assertion_delta.total_seconds()
|
||||
)
|
||||
|
||||
# adding a second follow-up at different intervals
|
||||
for delta in (timedelta(seconds=1), timedelta(minutes=1), timedelta(hours=1), timedelta(days=1), timedelta(days=10)):
|
||||
|
||||
for delta in (
|
||||
timedelta(seconds=1),
|
||||
timedelta(minutes=1),
|
||||
timedelta(hours=1),
|
||||
timedelta(days=1),
|
||||
timedelta(days=10),
|
||||
):
|
||||
followup2 = FollowUp.objects.create(
|
||||
ticket=ticket,
|
||||
date=followup1.date + delta,
|
||||
@ -103,16 +134,20 @@ class TimeSpentAutoTestCase(TestCase):
|
||||
user=self.user,
|
||||
new_status=1,
|
||||
message_id=uuid.uuid4().hex,
|
||||
time_spent=None
|
||||
time_spent=None,
|
||||
)
|
||||
|
||||
self.assertEqual(followup2.time_spent.total_seconds(), delta.total_seconds())
|
||||
self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds() + delta.total_seconds())
|
||||
self.assertEqual(
|
||||
followup2.time_spent.total_seconds(), delta.total_seconds()
|
||||
)
|
||||
self.assertEqual(
|
||||
ticket.time_spent.total_seconds(),
|
||||
assertion_delta.total_seconds() + delta.total_seconds(),
|
||||
)
|
||||
|
||||
# delete second follow-up as we test it with many intervals
|
||||
followup2.delete()
|
||||
|
||||
|
||||
def test_followup_time_spent_auto_opening_hours(self):
|
||||
"""Tests automatic time_spent calculation with opening hours and holidays."""
|
||||
|
||||
@ -130,45 +165,118 @@ class TimeSpentAutoTestCase(TestCase):
|
||||
|
||||
# adding holidays
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = (
|
||||
'2024-03-18', '2024-03-19', '2024-03-20', '2024-03-21', '2024-03-22',
|
||||
"2024-03-18",
|
||||
"2024-03-19",
|
||||
"2024-03-20",
|
||||
"2024-03-21",
|
||||
"2024-03-22",
|
||||
)
|
||||
|
||||
# ticket creation date, follow-up creation date, assertion value
|
||||
TEST_VALUES = (
|
||||
# monday
|
||||
('2024-03-04T00:00:00+00:00', '2024-03-04T09:30:10+00:00', timedelta(hours=9, minutes=30, seconds=10)),
|
||||
(
|
||||
"2024-03-04T00:00:00+00:00",
|
||||
"2024-03-04T09:30:10+00:00",
|
||||
timedelta(hours=9, minutes=30, seconds=10),
|
||||
),
|
||||
# tuesday
|
||||
('2024-03-05T07:00:00+00:00', '2024-03-05T09:00:00+00:00', timedelta(hours=1)),
|
||||
('2024-03-05T17:50:00+00:00', '2024-03-05T17:51:00+00:00', timedelta(minutes=1)),
|
||||
('2024-03-05T17:50:00+00:00', '2024-03-05T19:51:00+00:00', timedelta(minutes=10)),
|
||||
('2024-03-05T18:00:00+00:00', '2024-03-05T23:59:59+00:00', timedelta(hours=0)),
|
||||
('2024-03-05T20:00:00+00:00', '2024-03-05T20:59:59+00:00', timedelta(hours=0)),
|
||||
(
|
||||
"2024-03-05T07:00:00+00:00",
|
||||
"2024-03-05T09:00:00+00:00",
|
||||
timedelta(hours=1),
|
||||
),
|
||||
(
|
||||
"2024-03-05T17:50:00+00:00",
|
||||
"2024-03-05T17:51:00+00:00",
|
||||
timedelta(minutes=1),
|
||||
),
|
||||
(
|
||||
"2024-03-05T17:50:00+00:00",
|
||||
"2024-03-05T19:51:00+00:00",
|
||||
timedelta(minutes=10),
|
||||
),
|
||||
(
|
||||
"2024-03-05T18:00:00+00:00",
|
||||
"2024-03-05T23:59:59+00:00",
|
||||
timedelta(hours=0),
|
||||
),
|
||||
(
|
||||
"2024-03-05T20:00:00+00:00",
|
||||
"2024-03-05T20:59:59+00:00",
|
||||
timedelta(hours=0),
|
||||
),
|
||||
# wednesday
|
||||
('2024-03-06T08:00:00+00:00', '2024-03-06T09:01:00+00:00', timedelta(minutes=31)),
|
||||
('2024-03-06T01:00:00+00:00', '2024-03-06T19:30:10+00:00', timedelta(hours=10)),
|
||||
('2024-03-06T18:01:00+00:00', '2024-03-06T19:00:00+00:00', timedelta(minutes=29)),
|
||||
(
|
||||
"2024-03-06T08:00:00+00:00",
|
||||
"2024-03-06T09:01:00+00:00",
|
||||
timedelta(minutes=31),
|
||||
),
|
||||
(
|
||||
"2024-03-06T01:00:00+00:00",
|
||||
"2024-03-06T19:30:10+00:00",
|
||||
timedelta(hours=10),
|
||||
),
|
||||
(
|
||||
"2024-03-06T18:01:00+00:00",
|
||||
"2024-03-06T19:00:00+00:00",
|
||||
timedelta(minutes=29),
|
||||
),
|
||||
# thursday
|
||||
('2024-03-07T00:00:00+00:00', '2024-03-07T09:30:10+00:00', timedelta(hours=9, minutes=30, seconds=10)),
|
||||
('2024-03-07T09:30:00+00:00', '2024-03-07T10:30:00+00:00', timedelta(minutes=30)),
|
||||
(
|
||||
"2024-03-07T00:00:00+00:00",
|
||||
"2024-03-07T09:30:10+00:00",
|
||||
timedelta(hours=9, minutes=30, seconds=10),
|
||||
),
|
||||
(
|
||||
"2024-03-07T09:30:00+00:00",
|
||||
"2024-03-07T10:30:00+00:00",
|
||||
timedelta(minutes=30),
|
||||
),
|
||||
# friday
|
||||
('2024-03-08T00:00:00+00:00', '2024-03-08T23:30:10+00:00', timedelta(hours=10)),
|
||||
(
|
||||
"2024-03-08T00:00:00+00:00",
|
||||
"2024-03-08T23:30:10+00:00",
|
||||
timedelta(hours=10),
|
||||
),
|
||||
# saturday
|
||||
('2024-03-09T00:00:00+00:00', '2024-03-09T09:30:10+00:00', timedelta(hours=0)),
|
||||
(
|
||||
"2024-03-09T00:00:00+00:00",
|
||||
"2024-03-09T09:30:10+00:00",
|
||||
timedelta(hours=0),
|
||||
),
|
||||
# sunday
|
||||
('2024-03-10T00:00:00+00:00', '2024-03-10T09:30:10+00:00', timedelta(hours=0)),
|
||||
|
||||
(
|
||||
"2024-03-10T00:00:00+00:00",
|
||||
"2024-03-10T09:30:10+00:00",
|
||||
timedelta(hours=0),
|
||||
),
|
||||
# monday to sunday
|
||||
('2024-03-04T04:00:00+00:00', '2024-03-10T09:00:00+00:00', timedelta(hours=60)),
|
||||
|
||||
(
|
||||
"2024-03-04T04:00:00+00:00",
|
||||
"2024-03-10T09:00:00+00:00",
|
||||
timedelta(hours=60),
|
||||
),
|
||||
# two weeks
|
||||
('2024-03-04T04:00:00+00:00', '2024-03-17T09:00:00+00:00', timedelta(hours=124)),
|
||||
|
||||
(
|
||||
"2024-03-04T04:00:00+00:00",
|
||||
"2024-03-17T09:00:00+00:00",
|
||||
timedelta(hours=124),
|
||||
),
|
||||
# three weeks, the third one is holidays
|
||||
('2024-03-04T04:00:00+00:00', '2024-03-24T09:00:00+00:00', timedelta(hours=124)),
|
||||
('2024-03-18T04:00:00+00:00', '2024-03-24T09:00:00+00:00', timedelta(hours=0)),
|
||||
(
|
||||
"2024-03-04T04:00:00+00:00",
|
||||
"2024-03-24T09:00:00+00:00",
|
||||
timedelta(hours=124),
|
||||
),
|
||||
(
|
||||
"2024-03-18T04:00:00+00:00",
|
||||
"2024-03-24T09:00:00+00:00",
|
||||
timedelta(hours=0),
|
||||
),
|
||||
)
|
||||
|
||||
for (ticket_time, fup_time, assertion_delta) in TEST_VALUES:
|
||||
for ticket_time, fup_time, assertion_delta in TEST_VALUES:
|
||||
# create and setup test ticket time
|
||||
ticket = Ticket.objects.create(**self.ticket_data)
|
||||
ticket_time_p = datetime.strptime(ticket_time, "%Y-%m-%dT%H:%M:%S%z")
|
||||
@ -186,11 +294,15 @@ class TimeSpentAutoTestCase(TestCase):
|
||||
user=self.user,
|
||||
new_status=1,
|
||||
message_id=uuid.uuid4().hex,
|
||||
time_spent=None
|
||||
time_spent=None,
|
||||
)
|
||||
|
||||
self.assertEqual(followup1.time_spent.total_seconds(), assertion_delta.total_seconds())
|
||||
self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds())
|
||||
self.assertEqual(
|
||||
followup1.time_spent.total_seconds(), assertion_delta.total_seconds()
|
||||
)
|
||||
self.assertEqual(
|
||||
ticket.time_spent.total_seconds(), assertion_delta.total_seconds()
|
||||
)
|
||||
|
||||
# removing opening hours and holidays
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS = {}
|
||||
@ -205,15 +317,18 @@ class TimeSpentAutoTestCase(TestCase):
|
||||
# Follow-ups with OPEN_STATUS are excluded from time counting
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = (Ticket.OPEN_STATUS,)
|
||||
|
||||
|
||||
# create and setup test ticket time
|
||||
ticket = Ticket.objects.create(**self.ticket_data)
|
||||
ticket_time_p = datetime.strptime('2024-03-04T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z")
|
||||
ticket_time_p = datetime.strptime(
|
||||
"2024-03-04T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"
|
||||
)
|
||||
ticket.created = ticket_time_p
|
||||
ticket.modified = ticket_time_p
|
||||
ticket.save()
|
||||
|
||||
fup_time_p = datetime.strptime('2024-03-10T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z")
|
||||
fup_time_p = datetime.strptime(
|
||||
"2024-03-10T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"
|
||||
)
|
||||
followup1 = FollowUp.objects.create(
|
||||
ticket=ticket,
|
||||
date=fup_time_p,
|
||||
@ -223,7 +338,7 @@ class TimeSpentAutoTestCase(TestCase):
|
||||
user=self.user,
|
||||
new_status=1,
|
||||
message_id=uuid.uuid4().hex,
|
||||
time_spent=None
|
||||
time_spent=None,
|
||||
)
|
||||
|
||||
# The Follow-up time_spent should be zero as the default OPEN_STATUS was excluded from calculation
|
||||
@ -233,7 +348,6 @@ class TimeSpentAutoTestCase(TestCase):
|
||||
# Remove status exclusion
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = ()
|
||||
|
||||
|
||||
def test_followup_time_spent_auto_exclude_queues(self):
|
||||
"""Tests automatic time_spent calculation queues exclusion."""
|
||||
|
||||
@ -241,17 +355,20 @@ class TimeSpentAutoTestCase(TestCase):
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True
|
||||
|
||||
# Follow-ups within the default queue are excluded from time counting
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ('q1',)
|
||||
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ("q1",)
|
||||
|
||||
# create and setup test ticket time
|
||||
ticket = Ticket.objects.create(**self.ticket_data)
|
||||
ticket_time_p = datetime.strptime('2024-03-04T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z")
|
||||
ticket_time_p = datetime.strptime(
|
||||
"2024-03-04T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"
|
||||
)
|
||||
ticket.created = ticket_time_p
|
||||
ticket.modified = ticket_time_p
|
||||
ticket.save()
|
||||
|
||||
fup_time_p = datetime.strptime('2024-03-10T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z")
|
||||
fup_time_p = datetime.strptime(
|
||||
"2024-03-10T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"
|
||||
)
|
||||
followup1 = FollowUp.objects.create(
|
||||
ticket=ticket,
|
||||
date=fup_time_p,
|
||||
@ -261,7 +378,7 @@ class TimeSpentAutoTestCase(TestCase):
|
||||
user=self.user,
|
||||
new_status=1,
|
||||
message_id=uuid.uuid4().hex,
|
||||
time_spent=None
|
||||
time_spent=None,
|
||||
)
|
||||
|
||||
# The Follow-up time_spent should be zero as the default queue was excluded from calculation
|
||||
@ -276,13 +393,13 @@ class TimeSpentAutoTestCase(TestCase):
|
||||
|
||||
# activate automatic calculation
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ('stop1', 'stop2')
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ("stop1", "stop2")
|
||||
|
||||
# make staff user
|
||||
self.loginUser()
|
||||
|
||||
# create queues
|
||||
queues_sequence = ('new', 'stop1', 'resume1', 'stop2', 'resume2', 'end')
|
||||
queues_sequence = ("new", "stop1", "resume1", "stop2", "resume2", "end")
|
||||
queues = dict()
|
||||
for slug in queues_sequence:
|
||||
queues[slug] = Queue.objects.create(
|
||||
@ -292,34 +409,39 @@ class TimeSpentAutoTestCase(TestCase):
|
||||
|
||||
# create ticket
|
||||
initial_data = {
|
||||
'title': 'Queue change ticket test',
|
||||
'queue': queues['new'],
|
||||
'assigned_to': self.user,
|
||||
'status': Ticket.OPEN_STATUS,
|
||||
'created': datetime.strptime('2024-04-09T08:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z")
|
||||
"title": "Queue change ticket test",
|
||||
"queue": queues["new"],
|
||||
"assigned_to": self.user,
|
||||
"status": Ticket.OPEN_STATUS,
|
||||
"created": datetime.strptime(
|
||||
"2024-04-09T08:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"
|
||||
),
|
||||
}
|
||||
ticket = Ticket.objects.create(**initial_data)
|
||||
|
||||
# create a change queue follow-up every hour
|
||||
# first follow-up created at the same time of the ticket without queue change
|
||||
# new --1h--> stop1 --0h--> resume1 --1h--> stop2 --0h--> resume2 --1h--> end
|
||||
for (i, queue) in enumerate(queues_sequence):
|
||||
for i, queue in enumerate(queues_sequence):
|
||||
# create follow-up
|
||||
post_data = {
|
||||
'comment': 'ticket in queue {}'.format(queue),
|
||||
'queue': queues[queue].id,
|
||||
"comment": "ticket in queue {}".format(queue),
|
||||
"queue": queues[queue].id,
|
||||
}
|
||||
response = self.client.post(reverse('helpdesk:update', kwargs={
|
||||
'ticket_id': ticket.id}), post_data)
|
||||
latest_fup = ticket.followup_set.latest('id')
|
||||
response = self.client.post(
|
||||
reverse("helpdesk:update", kwargs={"ticket_id": ticket.id}), post_data
|
||||
)
|
||||
latest_fup = ticket.followup_set.latest("id")
|
||||
latest_fup.date = ticket.created + timedelta(hours=i)
|
||||
latest_fup.time_spent = None
|
||||
latest_fup.save()
|
||||
|
||||
|
||||
# total ticket time for followups is 5 hours
|
||||
self.assertEqual(latest_fup.date - ticket.created, timedelta(hours=5))
|
||||
# calculated time spent with 2 hours exclusion is 3 hours
|
||||
self.assertEqual(ticket.time_spent.total_seconds(), timedelta(hours=3).total_seconds())
|
||||
self.assertEqual(
|
||||
ticket.time_spent.total_seconds(), timedelta(hours=3).total_seconds()
|
||||
)
|
||||
|
||||
# remove queues exclusion
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ()
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ()
|
||||
|
@ -4,20 +4,18 @@ from django.urls import reverse
|
||||
|
||||
|
||||
class TicketActionsTestCase(TestCase):
|
||||
fixtures = ['emailtemplate.json']
|
||||
fixtures = ["emailtemplate.json"]
|
||||
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create(
|
||||
username='User_1',
|
||||
username="User_1",
|
||||
is_staff=True,
|
||||
)
|
||||
self.user.set_password('pass')
|
||||
self.user.set_password("pass")
|
||||
self.user.save()
|
||||
self.client.login(username='User_1', password='pass')
|
||||
self.client.login(username="User_1", password="pass")
|
||||
|
||||
def test_get_user_settings(self):
|
||||
|
||||
response = self.client.get(
|
||||
reverse('helpdesk:user_settings'), follow=True)
|
||||
response = self.client.get(reverse("helpdesk:user_settings"), follow=True)
|
||||
self.assertContains(response, "Use the following options")
|
||||
|
@ -1,9 +1,7 @@
|
||||
from django.contrib.auth.models import User
|
||||
from helpdesk.models import Queue, CustomField, TicketCustomFieldValue, Ticket
|
||||
from helpdesk.serializers import TicketSerializer
|
||||
from rest_framework.status import (
|
||||
HTTP_201_CREATED
|
||||
)
|
||||
from rest_framework.status import HTTP_201_CREATED
|
||||
from rest_framework.test import APITestCase
|
||||
import json
|
||||
import os
|
||||
@ -15,42 +13,53 @@ import http.server
|
||||
import threading
|
||||
from http import HTTPStatus
|
||||
|
||||
|
||||
class WebhookRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
server: "WebhookServer"
|
||||
|
||||
def do_POST(self):
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
content_length = int(self.headers["Content-Length"])
|
||||
body = self.rfile.read(content_length)
|
||||
self.server.requests.append({
|
||||
'path': self.path,
|
||||
'headers': self.headers,
|
||||
'body': body
|
||||
})
|
||||
if self.path == '/new-ticket':
|
||||
self.server.handled_new_ticket_requests.append(json.loads(body.decode('utf-8')))
|
||||
if self.path == '/new-ticket-1':
|
||||
self.server.handled_new_ticket_requests_1.append(json.loads(body.decode('utf-8')))
|
||||
elif self.path == '/followup':
|
||||
self.server.handled_follow_up_requests.append(json.loads(body.decode('utf-8')))
|
||||
elif self.path == '/followup-1':
|
||||
self.server.handled_follow_up_requests_1.append(json.loads(body.decode('utf-8')))
|
||||
self.server.requests.append(
|
||||
{"path": self.path, "headers": self.headers, "body": body}
|
||||
)
|
||||
if self.path == "/new-ticket":
|
||||
self.server.handled_new_ticket_requests.append(
|
||||
json.loads(body.decode("utf-8"))
|
||||
)
|
||||
if self.path == "/new-ticket-1":
|
||||
self.server.handled_new_ticket_requests_1.append(
|
||||
json.loads(body.decode("utf-8"))
|
||||
)
|
||||
elif self.path == "/followup":
|
||||
self.server.handled_follow_up_requests.append(
|
||||
json.loads(body.decode("utf-8"))
|
||||
)
|
||||
elif self.path == "/followup-1":
|
||||
self.server.handled_follow_up_requests_1.append(
|
||||
json.loads(body.decode("utf-8"))
|
||||
)
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
if not self.path == '/get-past-requests':
|
||||
if not self.path == "/get-past-requests":
|
||||
self.send_response(HTTPStatus.NOT_FOUND)
|
||||
self.end_headers()
|
||||
return
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.send_header("Content-type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({
|
||||
'new_ticket_requests': self.server.handled_new_ticket_requests,
|
||||
'new_ticket_requests_1': self.server.handled_new_ticket_requests_1,
|
||||
'follow_up_requests': self.server.handled_follow_up_requests,
|
||||
'follow_up_requests_1': self.server.handled_follow_up_requests_1
|
||||
}).encode('utf-8'))
|
||||
self.wfile.write(
|
||||
json.dumps(
|
||||
{
|
||||
"new_ticket_requests": self.server.handled_new_ticket_requests,
|
||||
"new_ticket_requests_1": self.server.handled_new_ticket_requests_1,
|
||||
"follow_up_requests": self.server.handled_follow_up_requests,
|
||||
"follow_up_requests_1": self.server.handled_follow_up_requests_1,
|
||||
}
|
||||
).encode("utf-8")
|
||||
)
|
||||
|
||||
|
||||
class WebhookServer(http.server.HTTPServer):
|
||||
@ -64,7 +73,9 @@ class WebhookServer(http.server.HTTPServer):
|
||||
|
||||
def start(self):
|
||||
self.thread = threading.Thread(target=self.serve_forever)
|
||||
self.thread.daemon = True # Set as a daemon so it will be killed once the main thread is dead
|
||||
self.thread.daemon = (
|
||||
True # Set as a daemon so it will be killed once the main thread is dead
|
||||
)
|
||||
self.thread.start()
|
||||
|
||||
def stop(self):
|
||||
@ -77,12 +88,12 @@ class WebhookTest(APITestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.queue = Queue.objects.create(
|
||||
title='Test Queue',
|
||||
slug='test-queue',
|
||||
title="Test Queue",
|
||||
slug="test-queue",
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
||||
staff_user = User.objects.create_user(username="test", is_staff=True)
|
||||
CustomField(
|
||||
name="my_custom_field",
|
||||
data_type="varchar",
|
||||
@ -91,82 +102,135 @@ class WebhookTest(APITestCase):
|
||||
self.client.force_authenticate(staff_user)
|
||||
|
||||
def test_test_server(self):
|
||||
server = WebhookServer(('localhost', 8123), WebhookRequestHandler)
|
||||
server = WebhookServer(("localhost", 8123), WebhookRequestHandler)
|
||||
server.start()
|
||||
requests.post('http://localhost:8123/new-ticket', json={
|
||||
"foo": "bar"})
|
||||
handled_webhook_requests = requests.get('http://localhost:8123/get-past-requests').json()
|
||||
self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["foo"], "bar")
|
||||
requests.post("http://localhost:8123/new-ticket", json={"foo": "bar"})
|
||||
handled_webhook_requests = requests.get(
|
||||
"http://localhost:8123/get-past-requests"
|
||||
).json()
|
||||
self.assertEqual(
|
||||
handled_webhook_requests["new_ticket_requests"][-1]["foo"], "bar"
|
||||
)
|
||||
server.stop()
|
||||
|
||||
def test_create_ticket_and_followup_via_api(self):
|
||||
server = WebhookServer(('localhost', 8124), WebhookRequestHandler)
|
||||
os.environ['HELPDESK_NEW_TICKET_WEBHOOK_URLS'] = 'http://localhost:8124/new-ticket, http://localhost:8124/new-ticket-1'
|
||||
os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8124/followup , http://localhost:8124/followup-1'
|
||||
server = WebhookServer(("localhost", 8124), WebhookRequestHandler)
|
||||
os.environ["HELPDESK_NEW_TICKET_WEBHOOK_URLS"] = (
|
||||
"http://localhost:8124/new-ticket, http://localhost:8124/new-ticket-1"
|
||||
)
|
||||
os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = (
|
||||
"http://localhost:8124/followup , http://localhost:8124/followup-1"
|
||||
)
|
||||
server.start()
|
||||
|
||||
response = self.client.post('/api/tickets/', {
|
||||
'queue': self.queue.id,
|
||||
'title': 'Test title',
|
||||
'description': 'Test description\nMulti lines',
|
||||
'submitter_email': 'test@mail.com',
|
||||
'priority': 4,
|
||||
'custom_my_custom_field': 'custom value',
|
||||
})
|
||||
response = self.client.post(
|
||||
"/api/tickets/",
|
||||
{
|
||||
"queue": self.queue.id,
|
||||
"title": "Test title",
|
||||
"description": "Test description\nMulti lines",
|
||||
"submitter_email": "test@mail.com",
|
||||
"priority": 4,
|
||||
"custom_my_custom_field": "custom value",
|
||||
},
|
||||
)
|
||||
self.assertEqual(CustomField.objects.all().first().name, "my_custom_field")
|
||||
self.assertEqual(TicketCustomFieldValue.objects.get(ticket=response.data['id']).value, 'custom value')
|
||||
self.assertEqual(
|
||||
TicketCustomFieldValue.objects.get(ticket=response.data["id"]).value,
|
||||
"custom value",
|
||||
)
|
||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||
handled_webhook_requests = requests.get('http://localhost:8124/get-past-requests')
|
||||
handled_webhook_requests = requests.get(
|
||||
"http://localhost:8124/get-past-requests"
|
||||
)
|
||||
handled_webhook_requests = handled_webhook_requests.json()
|
||||
self.assertTrue(len(handled_webhook_requests['new_ticket_requests']) == 1)
|
||||
self.assertTrue(len(handled_webhook_requests['new_ticket_requests_1']) == 1)
|
||||
self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 0)
|
||||
self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["title"], "Test title")
|
||||
self.assertEqual(handled_webhook_requests['new_ticket_requests_1'][-1]["ticket"]["title"], "Test title")
|
||||
self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["description"], "Test description\nMulti lines")
|
||||
ticket = Ticket.objects.get(id=handled_webhook_requests["new_ticket_requests"][-1]["ticket"]["id"])
|
||||
self.assertTrue(len(handled_webhook_requests["new_ticket_requests"]) == 1)
|
||||
self.assertTrue(len(handled_webhook_requests["new_ticket_requests_1"]) == 1)
|
||||
self.assertEqual(len(handled_webhook_requests["follow_up_requests"]), 0)
|
||||
self.assertEqual(
|
||||
handled_webhook_requests["new_ticket_requests"][-1]["ticket"]["title"],
|
||||
"Test title",
|
||||
)
|
||||
self.assertEqual(
|
||||
handled_webhook_requests["new_ticket_requests_1"][-1]["ticket"]["title"],
|
||||
"Test title",
|
||||
)
|
||||
self.assertEqual(
|
||||
handled_webhook_requests["new_ticket_requests"][-1]["ticket"][
|
||||
"description"
|
||||
],
|
||||
"Test description\nMulti lines",
|
||||
)
|
||||
ticket = Ticket.objects.get(
|
||||
id=handled_webhook_requests["new_ticket_requests"][-1]["ticket"]["id"]
|
||||
)
|
||||
ticket.set_custom_field_values()
|
||||
serializer = TicketSerializer(ticket)
|
||||
self.assertEqual(
|
||||
list(sorted(serializer.fields.keys())),
|
||||
['assigned_to',
|
||||
'attachment',
|
||||
'custom_my_custom_field',
|
||||
'description',
|
||||
'due_date',
|
||||
'followup_set',
|
||||
'id',
|
||||
'merged_to',
|
||||
'on_hold',
|
||||
'priority',
|
||||
'queue',
|
||||
'resolution',
|
||||
'status',
|
||||
'submitter_email',
|
||||
'title']
|
||||
[
|
||||
"assigned_to",
|
||||
"attachment",
|
||||
"custom_my_custom_field",
|
||||
"description",
|
||||
"due_date",
|
||||
"followup_set",
|
||||
"id",
|
||||
"merged_to",
|
||||
"on_hold",
|
||||
"priority",
|
||||
"queue",
|
||||
"resolution",
|
||||
"status",
|
||||
"submitter_email",
|
||||
"title",
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
serializer.data,
|
||||
handled_webhook_requests["new_ticket_requests"][-1]["ticket"],
|
||||
)
|
||||
response = self.client.post(
|
||||
"/api/followups/",
|
||||
{
|
||||
"ticket": handled_webhook_requests["new_ticket_requests"][-1]["ticket"][
|
||||
"id"
|
||||
],
|
||||
"comment": "Test comment",
|
||||
},
|
||||
)
|
||||
self.assertEqual(serializer.data, handled_webhook_requests["new_ticket_requests"][-1]["ticket"])
|
||||
response = self.client.post('/api/followups/', {
|
||||
'ticket': handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["id"],
|
||||
"comment": "Test comment",
|
||||
})
|
||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||
handled_webhook_requests = requests.get('http://localhost:8124/get-past-requests')
|
||||
handled_webhook_requests = requests.get(
|
||||
"http://localhost:8124/get-past-requests"
|
||||
)
|
||||
handled_webhook_requests = handled_webhook_requests.json()
|
||||
self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 1)
|
||||
self.assertEqual(len(handled_webhook_requests['follow_up_requests_1']), 1)
|
||||
self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["followup_set"][-1]["comment"], "Test comment")
|
||||
self.assertEqual(handled_webhook_requests['follow_up_requests_1'][-1]["ticket"]["followup_set"][-1]["comment"], "Test comment")
|
||||
|
||||
self.assertEqual(len(handled_webhook_requests["follow_up_requests"]), 1)
|
||||
self.assertEqual(len(handled_webhook_requests["follow_up_requests_1"]), 1)
|
||||
self.assertEqual(
|
||||
handled_webhook_requests["follow_up_requests"][-1]["ticket"][
|
||||
"followup_set"
|
||||
][-1]["comment"],
|
||||
"Test comment",
|
||||
)
|
||||
self.assertEqual(
|
||||
handled_webhook_requests["follow_up_requests_1"][-1]["ticket"][
|
||||
"followup_set"
|
||||
][-1]["comment"],
|
||||
"Test comment",
|
||||
)
|
||||
|
||||
server.stop()
|
||||
|
||||
def test_create_ticket_and_followup_via_email(self):
|
||||
from .. import email
|
||||
|
||||
server = WebhookServer(('localhost', 8125), WebhookRequestHandler)
|
||||
os.environ['HELPDESK_NEW_TICKET_WEBHOOK_URLS'] = 'http://localhost:8125/new-ticket'
|
||||
os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8125/followup'
|
||||
server = WebhookServer(("localhost", 8125), WebhookRequestHandler)
|
||||
os.environ["HELPDESK_NEW_TICKET_WEBHOOK_URLS"] = (
|
||||
"http://localhost:8125/new-ticket"
|
||||
)
|
||||
os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = "http://localhost:8125/followup"
|
||||
server.start()
|
||||
|
||||
class MockMessage(dict):
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
@ -175,13 +239,13 @@ class WebhookTest(APITestCase):
|
||||
return self.__dict__.get(key, default)
|
||||
|
||||
payload = {
|
||||
'body': "hello",
|
||||
'full_body': "hello",
|
||||
'subject': "Test subject",
|
||||
'queue': self.queue,
|
||||
'sender_email': "user@example.com",
|
||||
'priority': "1",
|
||||
'files': [],
|
||||
"body": "hello",
|
||||
"full_body": "hello",
|
||||
"subject": "Test subject",
|
||||
"queue": self.queue,
|
||||
"sender_email": "user@example.com",
|
||||
"priority": "1",
|
||||
"files": [],
|
||||
}
|
||||
|
||||
message = {
|
||||
@ -195,26 +259,29 @@ class WebhookTest(APITestCase):
|
||||
ticket_id=None,
|
||||
payload=payload,
|
||||
files=[],
|
||||
logger=logging.getLogger('helpdesk'),
|
||||
logger=logging.getLogger("helpdesk"),
|
||||
)
|
||||
|
||||
handled_webhook_requests = requests.get('http://localhost:8125/get-past-requests')
|
||||
handled_webhook_requests = requests.get(
|
||||
"http://localhost:8125/get-past-requests"
|
||||
)
|
||||
handled_webhook_requests = handled_webhook_requests.json()
|
||||
self.assertEqual(len(handled_webhook_requests['new_ticket_requests']), 1)
|
||||
self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 0)
|
||||
self.assertEqual(len(handled_webhook_requests["new_ticket_requests"]), 1)
|
||||
self.assertEqual(len(handled_webhook_requests["follow_up_requests"]), 0)
|
||||
|
||||
ticket_id = handled_webhook_requests['new_ticket_requests'][-1]["ticket"]['id']
|
||||
ticket_id = handled_webhook_requests["new_ticket_requests"][-1]["ticket"]["id"]
|
||||
from .. import models
|
||||
|
||||
ticket = models.Ticket.objects.get(id=ticket_id)
|
||||
|
||||
payload = {
|
||||
'body': "hello",
|
||||
'full_body': "hello",
|
||||
'subject': f"[test-queue-{ticket_id}] Test subject",
|
||||
'queue': self.queue,
|
||||
'sender_email': "user@example.com",
|
||||
'priority': "1",
|
||||
'files': [],
|
||||
"body": "hello",
|
||||
"full_body": "hello",
|
||||
"subject": f"[test-queue-{ticket_id}] Test subject",
|
||||
"queue": self.queue,
|
||||
"sender_email": "user@example.com",
|
||||
"priority": "1",
|
||||
"files": [],
|
||||
}
|
||||
|
||||
message = {
|
||||
@ -228,12 +295,22 @@ class WebhookTest(APITestCase):
|
||||
ticket_id=ticket_id,
|
||||
payload=payload,
|
||||
files=[],
|
||||
logger=logging.getLogger('helpdesk'),
|
||||
logger=logging.getLogger("helpdesk"),
|
||||
)
|
||||
handled_webhook_requests = requests.get(
|
||||
"http://localhost:8125/get-past-requests"
|
||||
)
|
||||
handled_webhook_requests = requests.get('http://localhost:8125/get-past-requests')
|
||||
handled_webhook_requests = handled_webhook_requests.json()
|
||||
self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 1)
|
||||
self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["followup_set"][-1]["comment"], "hello")
|
||||
self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["id"], ticket_id)
|
||||
self.assertEqual(len(handled_webhook_requests["follow_up_requests"]), 1)
|
||||
self.assertEqual(
|
||||
handled_webhook_requests["follow_up_requests"][-1]["ticket"][
|
||||
"followup_set"
|
||||
][-1]["comment"],
|
||||
"hello",
|
||||
)
|
||||
self.assertEqual(
|
||||
handled_webhook_requests["follow_up_requests"][-1]["ticket"]["id"],
|
||||
ticket_id,
|
||||
)
|
||||
|
||||
server.stop()
|
||||
|
@ -3,6 +3,6 @@ from django.urls import include, path
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', include('helpdesk.urls', namespace='helpdesk')),
|
||||
path('admin/', admin.site.urls),
|
||||
path("", include("helpdesk.urls", namespace="helpdesk")),
|
||||
path("admin/", admin.site.urls),
|
||||
]
|
||||
|
@ -1,6 +1,5 @@
|
||||
"""UItility functions facilitate making unit testing easier and less brittle."""
|
||||
|
||||
|
||||
from PIL import Image
|
||||
import email
|
||||
from email import encoders
|
||||
@ -31,8 +30,8 @@ def strip_accents(text):
|
||||
:returns: The processed String.
|
||||
:rtype: String.
|
||||
"""
|
||||
text = unicodedata.normalize('NFD', text)
|
||||
text = text.encode('ascii', 'ignore')
|
||||
text = unicodedata.normalize("NFD", text)
|
||||
text = text.encode("ascii", "ignore")
|
||||
text = text.decode("utf-8")
|
||||
return str(text)
|
||||
|
||||
@ -48,12 +47,12 @@ def text_to_id(text):
|
||||
:rtype: String.
|
||||
"""
|
||||
text = strip_accents(text.lower())
|
||||
text = re.sub('[ ]+', '_', text)
|
||||
text = re.sub('[^0-9a-zA-Z_-]', '', text)
|
||||
text = re.sub("[ ]+", "_", text)
|
||||
text = re.sub("[^0-9a-zA-Z_-]", "", text)
|
||||
return text
|
||||
|
||||
|
||||
def get_random_string(length: int=16) -> str:
|
||||
def get_random_string(length: int = 16) -> str:
|
||||
return "".join(
|
||||
[random.choice(string.ascii_letters + string.digits) for _ in range(length)]
|
||||
)
|
||||
@ -62,27 +61,27 @@ def get_random_string(length: int=16) -> str:
|
||||
def generate_random_image(image_format, array_dims):
|
||||
"""
|
||||
Creates an image from a random array.
|
||||
|
||||
|
||||
:param image_format: An image format (PNG or JPEG).
|
||||
:param array_dims: A tuple with array dimensions.
|
||||
|
||||
|
||||
:returns: A byte string with encoded image
|
||||
:rtype: bytes
|
||||
"""
|
||||
image_bytes = randint(low=0, high=255, size=array_dims, dtype='uint8')
|
||||
image_bytes = randint(low=0, high=255, size=array_dims, dtype="uint8")
|
||||
io = BytesIO()
|
||||
image_pil = Image.fromarray(image_bytes)
|
||||
image_pil.save(io, image_format, subsampling=0, quality=100)
|
||||
return io.getvalue()
|
||||
|
||||
|
||||
def get_random_image(image_format: str="PNG", size: int=5):
|
||||
def get_random_image(image_format: str = "PNG", size: int = 5):
|
||||
"""
|
||||
Returns a random image.
|
||||
|
||||
|
||||
Args:
|
||||
image_format: An image format (PNG or JPEG).
|
||||
|
||||
|
||||
Returns:
|
||||
A string with encoded image
|
||||
"""
|
||||
@ -92,120 +91,186 @@ def get_random_image(image_format: str="PNG", size: int=5):
|
||||
def get_fake(provider: str, locale: str = "en_US", min_length: int = 5) -> Any:
|
||||
"""
|
||||
Generates a random string, float, integer etc based on provider
|
||||
Provider can be "text', 'sentence', "word"
|
||||
e.g. `get_fake('name')` ==> 'Buzz Aldrin'
|
||||
Provider can be "text', 'sentence', "word"
|
||||
e.g. `get_fake('name')` ==> 'Buzz Aldrin'
|
||||
"""
|
||||
string = factory.Faker(provider).evaluate({}, None, {'locale': locale,})
|
||||
string = factory.Faker(provider).evaluate(
|
||||
{},
|
||||
None,
|
||||
{
|
||||
"locale": locale,
|
||||
},
|
||||
)
|
||||
while len(string) < min_length:
|
||||
string += factory.Faker(provider).evaluate({}, None, {'locale': locale,})
|
||||
string += factory.Faker(provider).evaluate(
|
||||
{},
|
||||
None,
|
||||
{
|
||||
"locale": locale,
|
||||
},
|
||||
)
|
||||
return string
|
||||
|
||||
|
||||
def get_fake_html(locale: str = "en_US", wrap_in_body_tag=True) -> Any:
|
||||
"""
|
||||
Generates a random string, float, integer etc based on provider
|
||||
Provider can be "text', 'sentence',
|
||||
e.g. `get_fake('name')` ==> 'Buzz Aldrin'
|
||||
Provider can be "text', 'sentence',
|
||||
e.g. `get_fake('name')` ==> 'Buzz Aldrin'
|
||||
"""
|
||||
html = factory.Faker("sentence").evaluate({}, None, {'locale': locale,})
|
||||
for _ in range(0,4):
|
||||
html += "<li>" + factory.Faker("sentence").evaluate({}, None, {'locale': locale,}) + "</li>"
|
||||
for _ in range(0,4):
|
||||
html += "<p>" + factory.Faker("text").evaluate({}, None, {'locale': locale,})
|
||||
html = factory.Faker("sentence").evaluate(
|
||||
{},
|
||||
None,
|
||||
{
|
||||
"locale": locale,
|
||||
},
|
||||
)
|
||||
for _ in range(0, 4):
|
||||
html += (
|
||||
"<li>"
|
||||
+ factory.Faker("sentence").evaluate(
|
||||
{},
|
||||
None,
|
||||
{
|
||||
"locale": locale,
|
||||
},
|
||||
)
|
||||
+ "</li>"
|
||||
)
|
||||
for _ in range(0, 4):
|
||||
html += "<p>" + factory.Faker("text").evaluate(
|
||||
{},
|
||||
None,
|
||||
{
|
||||
"locale": locale,
|
||||
},
|
||||
)
|
||||
return f"<body>{html}</body>" if wrap_in_body_tag else html
|
||||
|
||||
|
||||
def generate_email_address(
|
||||
locale: str="en_US",
|
||||
use_short_email: bool=False,
|
||||
real_name_format: Optional[str]="{last_name}, {first_name}",
|
||||
last_name_override: Optional[str]=None) -> Tuple[str, str, str, str]:
|
||||
'''
|
||||
locale: str = "en_US",
|
||||
use_short_email: bool = False,
|
||||
real_name_format: Optional[str] = "{last_name}, {first_name}",
|
||||
last_name_override: Optional[str] = None,
|
||||
) -> Tuple[str, str, str, str]:
|
||||
"""
|
||||
Generate an RFC 2822 email address
|
||||
|
||||
|
||||
:param locale: change this to generate locale specific names
|
||||
:param use_short_email: defaults to false. If true then does not include real name in email address
|
||||
:param real_name_format: pass a different format if different than "{last_name}, {first_name}"
|
||||
:param last_name_override: override the fake name if you want some special characters in the last name
|
||||
:returns <RFC2822 formatted email for header>, <short email address>, <first name>, <last_name
|
||||
'''
|
||||
"""
|
||||
fake = faker.Faker(locale=locale)
|
||||
first_name = fake.first_name()
|
||||
last_name = last_name_override or fake.last_name()
|
||||
real_name = None if use_short_email else real_name_format.format(first_name=first_name, last_name=last_name)
|
||||
real_name = (
|
||||
None
|
||||
if use_short_email
|
||||
else real_name_format.format(first_name=first_name, last_name=last_name)
|
||||
)
|
||||
# Add a random string to ensure we do not generate a real domain name
|
||||
email_address = "{}.{}@{}".format(
|
||||
first_name.replace(' ', '').encode("ascii", "ignore").lower().decode(),
|
||||
last_name.replace(' ', '').encode("ascii", "ignore").lower().decode(),
|
||||
get_random_string(5) + fake.domain_name()
|
||||
first_name.replace(" ", "").encode("ascii", "ignore").lower().decode(),
|
||||
last_name.replace(" ", "").encode("ascii", "ignore").lower().decode(),
|
||||
get_random_string(5) + fake.domain_name(),
|
||||
)
|
||||
# format email address for RFC 2822 and return
|
||||
return email.utils.formataddr((real_name, email_address)), email_address, first_name, last_name
|
||||
return (
|
||||
email.utils.formataddr((real_name, email_address)),
|
||||
email_address,
|
||||
first_name,
|
||||
last_name,
|
||||
)
|
||||
|
||||
def generate_file_mime_part(locale: str="en_US",filename: str = None, content: str = None) -> Message:
|
||||
|
||||
def generate_file_mime_part(
|
||||
locale: str = "en_US", filename: str = None, content: str = None
|
||||
) -> Message:
|
||||
"""
|
||||
|
||||
|
||||
:param locale: change this to generate locale specific file name and attachment content
|
||||
:param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated
|
||||
:param content: pass a string value if you want have specific content otherwise a random string will be generated
|
||||
"""
|
||||
part = MIMEBase('application', 'octet-stream')
|
||||
part.set_payload(get_fake("text", locale=locale, min_length=1024) if content is None else content)
|
||||
part = MIMEBase("application", "octet-stream")
|
||||
part.set_payload(
|
||||
get_fake("text", locale=locale, min_length=1024) if content is None else content
|
||||
)
|
||||
encoders.encode_base64(part)
|
||||
if not filename:
|
||||
filename = get_fake("word", locale=locale, min_length=8) + ".txt"
|
||||
part.add_header('Content-Disposition', "attachment; filename=%s" % filename)
|
||||
part.add_header("Content-Disposition", "attachment; filename=%s" % filename)
|
||||
return part
|
||||
|
||||
def generate_executable_mime_part(locale: str="en_US",filename: str = None, content: str = None) -> Message:
|
||||
|
||||
def generate_executable_mime_part(
|
||||
locale: str = "en_US", filename: str = None, content: str = None
|
||||
) -> Message:
|
||||
"""
|
||||
|
||||
|
||||
:param locale: change this to generate locale specific file name and attachment content
|
||||
:param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated
|
||||
:param content: pass a string value if you want have specific content otherwise a random string will be generated
|
||||
"""
|
||||
part = MIMEBase('application', 'vnd.microsoft.portable-executable')
|
||||
part.set_payload(get_fake("text", locale=locale, min_length=1024) if content is None else content)
|
||||
part = MIMEBase("application", "vnd.microsoft.portable-executable")
|
||||
part.set_payload(
|
||||
get_fake("text", locale=locale, min_length=1024) if content is None else content
|
||||
)
|
||||
encoders.encode_base64(part)
|
||||
if not filename:
|
||||
filename = get_fake("word", locale=locale, min_length=8) + ".exe"
|
||||
part.add_header('Content-Disposition', "attachment; filename=%s" % filename)
|
||||
part.add_header("Content-Disposition", "attachment; filename=%s" % filename)
|
||||
return part
|
||||
|
||||
def generate_image_mime_part(locale: str="en_US",imagename: str = None, disposition_primary_type: str = "attachment") -> Message:
|
||||
|
||||
def generate_image_mime_part(
|
||||
locale: str = "en_US",
|
||||
imagename: str = None,
|
||||
disposition_primary_type: str = "attachment",
|
||||
) -> Message:
|
||||
"""
|
||||
|
||||
|
||||
:param locale: change this to generate locale specific file name and attachment content
|
||||
:param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated
|
||||
"""
|
||||
part = MIMEImage(generate_random_image(image_format="JPEG", array_dims=(200, 200)))
|
||||
#part.set_payload(get_fake("text", locale=locale, min_length=1024))
|
||||
# part.set_payload(get_fake("text", locale=locale, min_length=1024))
|
||||
encoders.encode_base64(part)
|
||||
if not imagename:
|
||||
imagename = get_fake("word", locale=locale, min_length=8) + ".jpg"
|
||||
part.add_header('Content-Disposition', disposition_primary_type + "; filename= %s" % imagename)
|
||||
part.add_header(
|
||||
"Content-Disposition", disposition_primary_type + "; filename= %s" % imagename
|
||||
)
|
||||
return part
|
||||
|
||||
def generate_email_list(address_cnt: int = 3,
|
||||
locale: str="en_US",
|
||||
use_short_email: bool=False
|
||||
) -> str:
|
||||
|
||||
def generate_email_list(
|
||||
address_cnt: int = 3, locale: str = "en_US", use_short_email: bool = False
|
||||
) -> str:
|
||||
"""
|
||||
Generates a list of email addresses formatted for email headers on a Mime part
|
||||
|
||||
|
||||
:param address_cnt: the number of email addresses to string together
|
||||
:param locale: change this to generate locale specific "real names" and subject
|
||||
:param use_short_email: produces a email address without "real name" if True
|
||||
|
||||
"""
|
||||
email_address_list = [generate_email_address(locale, use_short_email=use_short_email)[0] for _ in range(0, address_cnt)]
|
||||
email_address_list = [
|
||||
generate_email_address(locale, use_short_email=use_short_email)[0]
|
||||
for _ in range(0, address_cnt)
|
||||
]
|
||||
return ",".join(email_address_list)
|
||||
|
||||
def add_simple_email_headers(message: Message, locale: str="en_US",
|
||||
use_short_email: bool=False
|
||||
) -> typing.Tuple[typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||
|
||||
def add_simple_email_headers(
|
||||
message: Message, locale: str = "en_US", use_short_email: bool = False
|
||||
) -> typing.Tuple[typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||
"""
|
||||
Adds the key email headers to a Mime part
|
||||
|
||||
|
||||
:param message: the Mime part to add headers to
|
||||
:param locale: change this to generate locale specific "real names" and subject
|
||||
:param use_short_email: produces a "To" or "From" that is only the email address if True
|
||||
@ -213,18 +278,20 @@ def add_simple_email_headers(message: Message, locale: str="en_US",
|
||||
"""
|
||||
to_meta = generate_email_address(locale, use_short_email=use_short_email)
|
||||
from_meta = generate_email_address(locale, use_short_email=use_short_email)
|
||||
|
||||
message['Subject'] = get_fake("sentence", locale=locale)
|
||||
message['From'] = from_meta[0]
|
||||
message['To'] = to_meta[0]
|
||||
|
||||
message["Subject"] = get_fake("sentence", locale=locale)
|
||||
message["From"] = from_meta[0]
|
||||
message["To"] = to_meta[0]
|
||||
return from_meta, to_meta
|
||||
|
||||
def generate_mime_part(locale: str="en_US",
|
||||
part_type: str="plain",
|
||||
) -> typing.Optional[Message]:
|
||||
|
||||
def generate_mime_part(
|
||||
locale: str = "en_US",
|
||||
part_type: str = "plain",
|
||||
) -> typing.Optional[Message]:
|
||||
"""
|
||||
Generates amime part of the sepecified type
|
||||
|
||||
|
||||
:param locale: change this to generate locale specific strings
|
||||
:param text_type: options are plain, html, image (attachment), file (attachment)
|
||||
"""
|
||||
@ -244,43 +311,53 @@ def generate_mime_part(locale: str="en_US",
|
||||
raise Exception("Mime part not implemented: " + part_type)
|
||||
return msg
|
||||
|
||||
def generate_multipart_email(locale: str="en_US",
|
||||
type_list: typing.List[str]=["plain", "html", "image"],
|
||||
sub_type: str = None,
|
||||
use_short_email: bool=False
|
||||
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||
|
||||
def generate_multipart_email(
|
||||
locale: str = "en_US",
|
||||
type_list: typing.List[str] = ["plain", "html", "image"],
|
||||
sub_type: str = None,
|
||||
use_short_email: bool = False,
|
||||
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||
"""
|
||||
Generates an email including headers with the defined multiparts
|
||||
|
||||
|
||||
:param locale:
|
||||
:param type_list: options are plain, html, image (attachment), file (attachment), and executable (.exe attachment)
|
||||
:param sub_type: multipart sub type that defaults to "mixed" if not specified
|
||||
:param use_short_email: produces a "To" or "From" that is only the email address if True
|
||||
"""
|
||||
"""
|
||||
msg = MIMEMultipart(sub_type) if sub_type else MIMEMultipart()
|
||||
for part_type in type_list:
|
||||
msg.attach(generate_mime_part(locale=locale, part_type=part_type))
|
||||
from_meta, to_meta = add_simple_email_headers(msg, locale=locale, use_short_email=use_short_email)
|
||||
from_meta, to_meta = add_simple_email_headers(
|
||||
msg, locale=locale, use_short_email=use_short_email
|
||||
)
|
||||
return msg, from_meta, to_meta
|
||||
|
||||
def generate_text_email(locale: str="en_US",
|
||||
use_short_email: bool=False
|
||||
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||
|
||||
def generate_text_email(
|
||||
locale: str = "en_US", use_short_email: bool = False
|
||||
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||
"""
|
||||
Generates an email including headers
|
||||
"""
|
||||
body = get_fake("text", locale=locale, min_length=1024)
|
||||
msg = MIMEText(body)
|
||||
from_meta, to_meta = add_simple_email_headers(msg, locale=locale, use_short_email=use_short_email)
|
||||
from_meta, to_meta = add_simple_email_headers(
|
||||
msg, locale=locale, use_short_email=use_short_email
|
||||
)
|
||||
return msg, from_meta, to_meta
|
||||
|
||||
def generate_html_email(locale: str="en_US",
|
||||
use_short_email: bool=False
|
||||
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||
|
||||
def generate_html_email(
|
||||
locale: str = "en_US", use_short_email: bool = False
|
||||
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||
"""
|
||||
Generates an email including headers
|
||||
"""
|
||||
body = get_fake_html(locale=locale)
|
||||
msg = MIMEText(body)
|
||||
from_meta, to_meta = add_simple_email_headers(msg, locale=locale, use_short_email=use_short_email)
|
||||
from_meta, to_meta = add_simple_email_headers(
|
||||
msg, locale=locale, use_short_email=use_short_email
|
||||
)
|
||||
return msg, from_meta, to_meta
|
||||
|
@ -20,15 +20,15 @@ from helpdesk.signals import update_ticket_done
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
def add_staff_subscription(
|
||||
user: User,
|
||||
ticket: Ticket
|
||||
) -> None:
|
||||
|
||||
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]:
|
||||
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)
|
||||
|
||||
|
||||
@ -45,7 +45,7 @@ def return_ticketccstring_and_show_subscribe(user, ticket):
|
||||
strings_to_check.append(username)
|
||||
strings_to_check.append(useremail)
|
||||
|
||||
ticketcc_string = ''
|
||||
ticketcc_string = ""
|
||||
all_ticketcc = ticket.ticketcc_set.all()
|
||||
counter_all_ticketcc = len(all_ticketcc) - 1
|
||||
show_subscribe = True
|
||||
@ -53,7 +53,7 @@ def return_ticketccstring_and_show_subscribe(user, ticket):
|
||||
ticketcc_this_entry = str(ticketcc.display)
|
||||
ticketcc_string += ticketcc_this_entry
|
||||
if i < counter_all_ticketcc:
|
||||
ticketcc_string += ', '
|
||||
ticketcc_string += ", "
|
||||
if strings_to_check.__contains__(ticketcc_this_entry.upper()):
|
||||
show_subscribe = False
|
||||
|
||||
@ -64,18 +64,19 @@ def return_ticketccstring_and_show_subscribe(user, ticket):
|
||||
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):
|
||||
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):
|
||||
|
||||
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)
|
||||
queryset = TicketCC.objects.filter(ticket=ticket, user=user, email=email)
|
||||
|
||||
# Don't create duplicate entries for subscribers
|
||||
if queryset.count() > 0:
|
||||
@ -83,21 +84,19 @@ def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, ca
|
||||
|
||||
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)
|
||||
_(
|
||||
"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
|
||||
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
|
||||
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.
|
||||
@ -112,15 +111,15 @@ def get_and_set_ticket_status(
|
||||
ticket.save()
|
||||
follow_up.new_status = new_status
|
||||
if follow_up.title:
|
||||
follow_up.title += ' and %s' % ticket.get_status_display()
|
||||
follow_up.title += " and %s" % ticket.get_status_display()
|
||||
else:
|
||||
follow_up.title = '%s' % ticket.get_status_display()
|
||||
follow_up.title = "%s" % ticket.get_status_display()
|
||||
|
||||
if not follow_up.title:
|
||||
if follow_up.comment:
|
||||
follow_up.title = _('Comment')
|
||||
follow_up.title = _("Comment")
|
||||
else:
|
||||
follow_up.title = _('Updated')
|
||||
follow_up.title = _("Updated")
|
||||
|
||||
follow_up.save()
|
||||
return old_status_str, old_status
|
||||
@ -132,80 +131,76 @@ def update_messages_sent_to_by_public_and_status(
|
||||
follow_up: FollowUp,
|
||||
context: str,
|
||||
messages_sent_to: typing.Set[str],
|
||||
files: typing.List[typing.Tuple[str, 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
|
||||
)
|
||||
)
|
||||
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_'
|
||||
template = "resolved_"
|
||||
elif follow_up.new_status == Ticket.CLOSED_STATUS:
|
||||
template = 'closed_'
|
||||
template = "closed_"
|
||||
else:
|
||||
template = 'updated_'
|
||||
template = "updated_"
|
||||
|
||||
roles = {
|
||||
'submitter': (template + 'submitter', context),
|
||||
'ticket_cc': (template + 'cc', context),
|
||||
"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)
|
||||
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
|
||||
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
|
||||
reassigned, follow_up: FollowUp
|
||||
) -> typing.Tuple[str, str]:
|
||||
if reassigned:
|
||||
template_staff = 'assigned_owner'
|
||||
template_staff = "assigned_owner"
|
||||
elif follow_up.new_status == Ticket.RESOLVED_STATUS:
|
||||
template_staff = 'resolved_owner'
|
||||
template_staff = "resolved_owner"
|
||||
elif follow_up.new_status == Ticket.CLOSED_STATUS:
|
||||
template_staff = 'closed_owner'
|
||||
template_staff = "closed_owner"
|
||||
else:
|
||||
template_staff = 'updated_owner'
|
||||
template_staff = "updated_owner"
|
||||
if reassigned:
|
||||
template_cc = 'assigned_cc'
|
||||
template_cc = "assigned_cc"
|
||||
elif follow_up.new_status == Ticket.RESOLVED_STATUS:
|
||||
template_cc = 'resolved_cc'
|
||||
template_cc = "resolved_cc"
|
||||
elif follow_up.new_status == Ticket.CLOSED_STATUS:
|
||||
template_cc = 'closed_cc'
|
||||
template_cc = "closed_cc"
|
||||
else:
|
||||
template_cc = 'updated_cc'
|
||||
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,
|
||||
queue=-1,
|
||||
new_status=None,
|
||||
time_spent=None,
|
||||
due_date=None,
|
||||
new_checklists=None,
|
||||
message_id=None,
|
||||
customfields_form=None,
|
||||
user,
|
||||
ticket,
|
||||
title=None,
|
||||
comment="",
|
||||
files=None,
|
||||
public=False,
|
||||
owner=-1,
|
||||
priority=-1,
|
||||
queue=-1,
|
||||
new_status=None,
|
||||
time_spent=None,
|
||||
due_date=None,
|
||||
new_checklists=None,
|
||||
message_id=None,
|
||||
customfields_form=None,
|
||||
):
|
||||
# We need to allow the 'ticket' and 'queue' contexts to be applied to the
|
||||
# comment.
|
||||
@ -222,25 +217,31 @@ def update_ticket(
|
||||
new_checklists = {}
|
||||
|
||||
from django.template import engines
|
||||
template_func = engines['django'].from_string
|
||||
|
||||
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 %}'
|
||||
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, message_id=message_id, title=title)
|
||||
f = FollowUp(
|
||||
ticket=ticket,
|
||||
date=timezone.now(),
|
||||
comment=comment,
|
||||
time_spent=time_spent,
|
||||
message_id=message_id,
|
||||
title=title,
|
||||
)
|
||||
|
||||
if is_helpdesk_staff(user):
|
||||
f.user = user
|
||||
@ -251,16 +252,19 @@ def update_ticket(
|
||||
|
||||
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):
|
||||
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(),
|
||||
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')
|
||||
f.title = _("Unassigned")
|
||||
ticket.assigned_to = None
|
||||
|
||||
old_status_str, old_status = get_and_set_ticket_status(new_status, ticket, f)
|
||||
@ -269,7 +273,7 @@ def update_ticket(
|
||||
|
||||
if title and title != ticket.title:
|
||||
c = f.ticketchange_set.create(
|
||||
field=_('Title'),
|
||||
field=_("Title"),
|
||||
old_value=ticket.title,
|
||||
new_value=title,
|
||||
)
|
||||
@ -277,21 +281,21 @@ def update_ticket(
|
||||
|
||||
if new_status != old_status:
|
||||
c = f.ticketchange_set.create(
|
||||
field=_('Status'),
|
||||
field=_("Status"),
|
||||
old_value=old_status_str,
|
||||
new_value=ticket.get_status_display(),
|
||||
)
|
||||
|
||||
if ticket.assigned_to != old_owner:
|
||||
c = f.ticketchange_set.create(
|
||||
field=_('Owner'),
|
||||
field=_("Owner"),
|
||||
old_value=old_owner,
|
||||
new_value=ticket.assigned_to,
|
||||
)
|
||||
|
||||
if priority != ticket.priority:
|
||||
c = f.ticketchange_set.create(
|
||||
field=_('Priority'),
|
||||
field=_("Priority"),
|
||||
old_value=ticket.priority,
|
||||
new_value=priority,
|
||||
)
|
||||
@ -299,7 +303,7 @@ def update_ticket(
|
||||
|
||||
if queue != ticket.queue.id:
|
||||
c = f.ticketchange_set.create(
|
||||
field=_('Queue'),
|
||||
field=_("Queue"),
|
||||
old_value=ticket.queue.id,
|
||||
new_value=queue,
|
||||
)
|
||||
@ -307,16 +311,16 @@ def update_ticket(
|
||||
|
||||
if due_date != ticket.due_date:
|
||||
c = f.ticketchange_set.create(
|
||||
field=_('Due on'),
|
||||
field=_("Due on"),
|
||||
old_value=ticket.due_date,
|
||||
new_value=due_date,
|
||||
)
|
||||
ticket.due_date = due_date
|
||||
|
||||
|
||||
# save custom fields and ticket changes
|
||||
if customfields_form and customfields_form.is_valid():
|
||||
customfields_form.save(followup=f)
|
||||
|
||||
|
||||
for checklist in ticket.checklists.all():
|
||||
if checklist.id not in new_checklists:
|
||||
continue
|
||||
@ -327,24 +331,22 @@ def update_ticket(
|
||||
# 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'
|
||||
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'
|
||||
changed = "uncompleted"
|
||||
|
||||
# Save and add ticket change if task state has changed
|
||||
if changed:
|
||||
task.save(update_fields=['completion_date'])
|
||||
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'),
|
||||
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 (
|
||||
if new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS) and (
|
||||
new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None
|
||||
):
|
||||
ticket.resolution = comment
|
||||
@ -363,32 +365,34 @@ def update_ticket(
|
||||
except AttributeError:
|
||||
pass
|
||||
ticket = update_messages_sent_to_by_public_and_status(
|
||||
public,
|
||||
ticket,
|
||||
f,
|
||||
context,
|
||||
messages_sent_to,
|
||||
files
|
||||
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)
|
||||
or (
|
||||
reassigned
|
||||
and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign
|
||||
)
|
||||
):
|
||||
messages_sent_to.update(ticket.send(
|
||||
{'assigned_to': (template_staff, context)},
|
||||
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,
|
||||
))
|
||||
|
||||
messages_sent_to.update(ticket.send(
|
||||
{'ticket_cc': (template_cc, context)},
|
||||
dont_send_to=messages_sent_to,
|
||||
fail_silently=True,
|
||||
files=files,
|
||||
))
|
||||
)
|
||||
)
|
||||
ticket.save()
|
||||
|
||||
# emit signal with followup when the ticket update is done
|
||||
@ -398,4 +402,3 @@ def update_ticket(
|
||||
# auto subscribe user if enabled
|
||||
add_staff_subscription(user, ticket)
|
||||
return f
|
||||
|
||||
|
@ -14,7 +14,13 @@ from django.views.generic import TemplateView
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.decorators import helpdesk_staff_member_required, protect_view
|
||||
from helpdesk.views import feeds, login, public, staff
|
||||
from helpdesk.views.api import CreateUserView, FollowUpAttachmentViewSet, FollowUpViewSet, TicketViewSet, UserTicketViewSet
|
||||
from helpdesk.views.api import (
|
||||
CreateUserView,
|
||||
FollowUpAttachmentViewSet,
|
||||
FollowUpViewSet,
|
||||
TicketViewSet,
|
||||
UserTicketViewSet,
|
||||
)
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
|
||||
@ -63,16 +69,12 @@ urlpatterns = [
|
||||
name="followup_delete",
|
||||
),
|
||||
path("tickets/<int:ticket_id>/edit/", staff.edit_ticket, name="edit"),
|
||||
path("tickets/<int:ticket_id>/update/",
|
||||
staff.update_ticket_view, name="update"),
|
||||
path("tickets/<int:ticket_id>/delete/",
|
||||
staff.delete_ticket, name="delete"),
|
||||
path("tickets/<int:ticket_id>/update/", staff.update_ticket_view, name="update"),
|
||||
path("tickets/<int:ticket_id>/delete/", staff.delete_ticket, name="delete"),
|
||||
path("tickets/<int:ticket_id>/hold/", staff.hold_ticket, name="hold"),
|
||||
path("tickets/<int:ticket_id>/unhold/",
|
||||
staff.unhold_ticket, name="unhold"),
|
||||
path("tickets/<int:ticket_id>/unhold/", staff.unhold_ticket, name="unhold"),
|
||||
path("tickets/<int:ticket_id>/cc/", staff.ticket_cc, name="ticket_cc"),
|
||||
path("tickets/<int:ticket_id>/cc/add/",
|
||||
staff.ticket_cc_add, name="ticket_cc_add"),
|
||||
path("tickets/<int:ticket_id>/cc/add/", staff.ticket_cc_add, name="ticket_cc_add"),
|
||||
path(
|
||||
"tickets/<int:ticket_id>/cc/delete/<int:cc_id>/",
|
||||
staff.ticket_cc_del,
|
||||
@ -106,35 +108,33 @@ urlpatterns = [
|
||||
path(
|
||||
"tickets/<int:ticket_id>/checklists/<int:checklist_id>/",
|
||||
staff.edit_ticket_checklist,
|
||||
name="edit_ticket_checklist"
|
||||
name="edit_ticket_checklist",
|
||||
),
|
||||
path(
|
||||
"tickets/<int:ticket_id>/checklists/<int:checklist_id>/delete/",
|
||||
staff.delete_ticket_checklist,
|
||||
name="delete_ticket_checklist"
|
||||
name="delete_ticket_checklist",
|
||||
),
|
||||
re_path(r"^raw/(?P<type_>\w+)/$", staff.raw_details, name="raw"),
|
||||
path("rss/", staff.rss_list, name="rss_index"),
|
||||
path("reports/", staff.report_index, name="report_index"),
|
||||
re_path(r"^reports/(?P<report>\w+)/$",
|
||||
staff.run_report, name="run_report"),
|
||||
re_path(r"^reports/(?P<report>\w+)/$", staff.run_report, name="run_report"),
|
||||
path("save_query/", staff.save_query, name="savequery"),
|
||||
path("delete_query/<int:pk>/", staff.delete_saved_query, name="delete_query"),
|
||||
path("settings/", staff.EditUserSettingsView.as_view(), name="user_settings"),
|
||||
path("ignore/", staff.email_ignore, name="email_ignore"),
|
||||
path("ignore/add/", staff.email_ignore_add, name="email_ignore_add"),
|
||||
path("ignore/delete/<int:id>/",
|
||||
staff.email_ignore_del, name="email_ignore_del"),
|
||||
path("ignore/delete/<int:id>/", staff.email_ignore_del, name="email_ignore_del"),
|
||||
path("checklist-templates/", staff.checklist_templates, name="checklist_templates"),
|
||||
path(
|
||||
"checklist-templates/<int:checklist_template_id>/",
|
||||
staff.checklist_templates,
|
||||
name="edit_checklist_template"
|
||||
name="edit_checklist_template",
|
||||
),
|
||||
path(
|
||||
"checklist-templates/<int:checklist_template_id>/delete/",
|
||||
staff.delete_checklist_template,
|
||||
name="delete_checklist_template"
|
||||
name="delete_checklist_template",
|
||||
),
|
||||
re_path(
|
||||
r"^datatables_ticket_list/(?P<query>{})$".format(base64_pattern),
|
||||
@ -164,7 +164,11 @@ if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET:
|
||||
|
||||
urlpatterns += [
|
||||
path("", protect_view(public.Homepage.as_view()), name="home"),
|
||||
path("tickets/my-tickets/", protect_view(public.MyTickets.as_view()), name="my-tickets"),
|
||||
path(
|
||||
"tickets/my-tickets/",
|
||||
protect_view(public.MyTickets.as_view()),
|
||||
name="my-tickets",
|
||||
),
|
||||
path("tickets/submit/", public.create_ticket, name="submit"),
|
||||
path(
|
||||
"tickets/submit_iframe/",
|
||||
@ -177,8 +181,7 @@ urlpatterns += [
|
||||
name="success_iframe",
|
||||
),
|
||||
path("view/", protect_view(public.ViewTicket.as_view()), name="public_view"),
|
||||
path("change_language/", public.change_language,
|
||||
name="public_change_language"),
|
||||
path("change_language/", public.change_language, name="public_change_language"),
|
||||
]
|
||||
|
||||
urlpatterns += [
|
||||
@ -214,8 +217,9 @@ router = DefaultRouter()
|
||||
router.register(r"tickets", TicketViewSet, basename="ticket")
|
||||
router.register(r"user_tickets", UserTicketViewSet, basename="user_tickets")
|
||||
router.register(r"followups", FollowUpViewSet, basename="followups")
|
||||
router.register(r"followups-attachments",
|
||||
FollowUpAttachmentViewSet, basename="followupattachments")
|
||||
router.register(
|
||||
r"followups-attachments", FollowUpAttachmentViewSet, basename="followupattachments"
|
||||
)
|
||||
router.register(r"users", CreateUserView, basename="user")
|
||||
urlpatterns += [re_path(r"^api/", include(router.urls))]
|
||||
|
||||
@ -249,8 +253,7 @@ urlpatterns += [
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
urlpatterns += [
|
||||
path("kb/", kb.index, name="kb_index"),
|
||||
re_path(r"^kb/(?P<slug>[A-Za-z0-9_-]+)/$",
|
||||
kb.category, name="kb_category"),
|
||||
re_path(r"^kb/(?P<slug>[A-Za-z0-9_-]+)/$", kb.category, name="kb_category"),
|
||||
re_path(r"^kb/(?P<item>\d+)/vote/(?P<vote>up|down)/$", kb.vote, name="kb_vote"),
|
||||
re_path(
|
||||
r"^kb_iframe/(?P<slug>[A-Za-z0-9_-]+)/$",
|
||||
@ -268,8 +271,7 @@ urlpatterns += [
|
||||
path(
|
||||
"system_settings/",
|
||||
login_required(
|
||||
DirectTemplateView.as_view(
|
||||
template_name="helpdesk/system_settings.html")
|
||||
DirectTemplateView.as_view(template_name="helpdesk/system_settings.html")
|
||||
),
|
||||
name="system_settings",
|
||||
),
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.models import Queue, Ticket
|
||||
|
||||
@ -23,14 +22,13 @@ class HelpdeskUser:
|
||||
"""
|
||||
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 \
|
||||
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 = [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:
|
||||
@ -56,8 +54,11 @@ class HelpdeskUser:
|
||||
return Ticket.objects.filter(queue__in=self.get_queues())
|
||||
|
||||
def has_full_access(self):
|
||||
return self.user.is_superuser or self.user.is_staff \
|
||||
return (
|
||||
self.user.is_superuser
|
||||
or self.user.is_staff
|
||||
or helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
||||
)
|
||||
|
||||
def can_access_queue(self, queue):
|
||||
"""Check if a certain user can access a certain queue.
|
||||
@ -71,18 +72,18 @@ class HelpdeskUser:
|
||||
else:
|
||||
return (
|
||||
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION
|
||||
and
|
||||
self.user.has_perm(queue.permission_name)
|
||||
and self.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."""
|
||||
a ticket. If not then deny access."""
|
||||
user = self.user
|
||||
if self.can_access_queue(ticket.queue):
|
||||
return True
|
||||
elif self.has_full_access() or \
|
||||
(ticket.assigned_to and user.id == ticket.assigned_to.id):
|
||||
elif self.has_full_access() or (
|
||||
ticket.assigned_to and user.id == ticket.assigned_to.id
|
||||
):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@ -90,4 +91,6 @@ class HelpdeskUser:
|
||||
def can_access_kbcategory(self, category):
|
||||
if category.public:
|
||||
return True
|
||||
return self.has_full_access() or (category.queue and self.can_access_queue(category.queue))
|
||||
return self.has_full_access() or (
|
||||
category.queue and self.can_access_queue(category.queue)
|
||||
)
|
||||
|
@ -14,6 +14,7 @@ from helpdesk import settings as helpdesk_settings
|
||||
def validate_file_extension(value):
|
||||
from django.core.exceptions import ValidationError
|
||||
import os
|
||||
|
||||
ext = os.path.splitext(value.name)[1] # [0] returns path+filename
|
||||
# TODO: we might improve this with more thorough checks of file types
|
||||
# rather than just the extensions.
|
||||
@ -24,7 +25,5 @@ def validate_file_extension(value):
|
||||
if ext.lower() not in helpdesk_settings.HELPDESK_VALID_EXTENSIONS:
|
||||
# TODO: one more check in case it is a file with no extension; we
|
||||
# should always allow that?
|
||||
if not (ext.lower() == '' or ext.lower() == '.'):
|
||||
raise ValidationError(
|
||||
_('Unsupported file extension: ') + ext.lower()
|
||||
)
|
||||
if not (ext.lower() == "" or ext.lower() == "."):
|
||||
raise ValidationError(_("Unsupported file extension: ") + ext.lower())
|
||||
|
@ -1,23 +1,28 @@
|
||||
from helpdesk.models import CustomField, KBItem, Queue
|
||||
|
||||
|
||||
class AbstractCreateTicketMixin():
|
||||
class AbstractCreateTicketMixin:
|
||||
def get_initial(self):
|
||||
initial_data = {}
|
||||
request = self.request
|
||||
try:
|
||||
initial_data['queue'] = Queue.objects.get(
|
||||
slug=request.GET.get('queue', None)).id
|
||||
initial_data["queue"] = Queue.objects.get(
|
||||
slug=request.GET.get("queue", None)
|
||||
).id
|
||||
except Queue.DoesNotExist:
|
||||
pass
|
||||
u = request.user
|
||||
if u.is_authenticated and u.usersettings_helpdesk.use_email_as_submitter and u.email:
|
||||
initial_data['submitter_email'] = u.email
|
||||
if (
|
||||
u.is_authenticated
|
||||
and u.usersettings_helpdesk.use_email_as_submitter
|
||||
and u.email
|
||||
):
|
||||
initial_data["submitter_email"] = u.email
|
||||
|
||||
query_param_fields = ['submitter_email',
|
||||
'title', 'body', 'queue', 'kbitem']
|
||||
query_param_fields = ["submitter_email", "title", "body", "queue", "kbitem"]
|
||||
custom_fields = [
|
||||
"custom_%s" % f.name for f in CustomField.objects.filter(staff_only=False)]
|
||||
"custom_%s" % f.name for f in CustomField.objects.filter(staff_only=False)
|
||||
]
|
||||
query_param_fields += custom_fields
|
||||
for qpf in query_param_fields:
|
||||
initial_data[qpf] = request.GET.get(qpf, initial_data.get(qpf, ""))
|
||||
@ -27,13 +32,12 @@ class AbstractCreateTicketMixin():
|
||||
def get_form_kwargs(self, *args, **kwargs):
|
||||
kwargs = super().get_form_kwargs(*args, **kwargs)
|
||||
kbitem = self.request.GET.get(
|
||||
'kbitem',
|
||||
self.request.POST.get('kbitem', None),
|
||||
"kbitem",
|
||||
self.request.POST.get("kbitem", None),
|
||||
)
|
||||
if kbitem:
|
||||
try:
|
||||
kwargs['kbcategory'] = KBItem.objects.get(
|
||||
pk=int(kbitem)).category
|
||||
kwargs["kbcategory"] = KBItem.objects.get(pk=int(kbitem)).category
|
||||
except (ValueError, KBItem.DoesNotExist):
|
||||
pass
|
||||
return kwargs
|
||||
|
@ -1,6 +1,12 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from helpdesk.models import FollowUp, FollowUpAttachment, Ticket
|
||||
from helpdesk.serializers import FollowUpAttachmentSerializer, FollowUpSerializer, TicketSerializer, UserSerializer, PublicTicketListingSerializer
|
||||
from helpdesk.serializers import (
|
||||
FollowUpAttachmentSerializer,
|
||||
FollowUpSerializer,
|
||||
TicketSerializer,
|
||||
UserSerializer,
|
||||
PublicTicketListingSerializer,
|
||||
)
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.mixins import CreateModelMixin
|
||||
from rest_framework.permissions import IsAdminUser, IsAuthenticated
|
||||
@ -12,7 +18,7 @@ from helpdesk import settings as helpdesk_settings
|
||||
|
||||
class ConservativePagination(PageNumberPagination):
|
||||
page_size = 25
|
||||
page_size_query_param = 'page_size'
|
||||
page_size_query_param = "page_size"
|
||||
|
||||
|
||||
class UserTicketViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
@ -21,18 +27,20 @@ class UserTicketViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
The view is paginated by default
|
||||
"""
|
||||
|
||||
serializer_class = PublicTicketListingSerializer
|
||||
pagination_class = ConservativePagination
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
tickets = Ticket.objects.filter(submitter_email=self.request.user.email).order_by('-created')
|
||||
tickets = Ticket.objects.filter(
|
||||
submitter_email=self.request.user.email
|
||||
).order_by("-created")
|
||||
for ticket in tickets:
|
||||
ticket.set_custom_field_values()
|
||||
return tickets
|
||||
|
||||
|
||||
|
||||
class TicketViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
A viewset that provides the standard actions to handle Ticket
|
||||
@ -41,6 +49,7 @@ class TicketViewSet(viewsets.ModelViewSet):
|
||||
|
||||
`/api/tickets/?status=Open,Resolved` will return all the tickets that are Open or Resolved.
|
||||
"""
|
||||
|
||||
queryset = Ticket.objects.all()
|
||||
serializer_class = TicketSerializer
|
||||
pagination_class = ConservativePagination
|
||||
@ -50,17 +59,17 @@ class TicketViewSet(viewsets.ModelViewSet):
|
||||
tickets = Ticket.objects.all()
|
||||
|
||||
# filter by status
|
||||
status = self.request.query_params.get('status', None)
|
||||
status = self.request.query_params.get("status", None)
|
||||
if status:
|
||||
statuses = status.split(',') if status else []
|
||||
status_choices = helpdesk_settings.TICKET_STATUS_CHOICES
|
||||
number_statuses = []
|
||||
for status in statuses:
|
||||
for choice in status_choices:
|
||||
if str(choice[0]) == status:
|
||||
number_statuses.append(choice[0])
|
||||
if number_statuses:
|
||||
tickets = tickets.filter(status__in=number_statuses)
|
||||
statuses = status.split(",") if status else []
|
||||
status_choices = helpdesk_settings.TICKET_STATUS_CHOICES
|
||||
number_statuses = []
|
||||
for status in statuses:
|
||||
for choice in status_choices:
|
||||
if str(choice[0]) == status:
|
||||
number_statuses.append(choice[0])
|
||||
if number_statuses:
|
||||
tickets = tickets.filter(status__in=number_statuses)
|
||||
|
||||
for ticket in tickets:
|
||||
ticket.set_custom_field_values()
|
||||
|
@ -25,8 +25,8 @@ for open_status in Ticket.OPEN_STATUSES:
|
||||
|
||||
|
||||
class OpenTicketsByUser(Feed):
|
||||
title_template = 'helpdesk/rss/ticket_title.html'
|
||||
description_template = 'helpdesk/rss/ticket_description.html'
|
||||
title_template = "helpdesk/rss/ticket_title.html"
|
||||
description_template = "helpdesk/rss/ticket_description.html"
|
||||
|
||||
def get_object(self, request, user_name, queue_slug=None):
|
||||
user = get_object_or_404(User, username=user_name)
|
||||
@ -35,54 +35,56 @@ class OpenTicketsByUser(Feed):
|
||||
else:
|
||||
queue = None
|
||||
|
||||
return {'user': user, 'queue': queue}
|
||||
return {"user": user, "queue": queue}
|
||||
|
||||
def title(self, obj):
|
||||
if obj['queue']:
|
||||
if obj["queue"]:
|
||||
return _("Helpdesk: Open Tickets in queue %(queue)s for %(username)s") % {
|
||||
'queue': obj['queue'].title,
|
||||
'username': obj['user'].get_username(),
|
||||
"queue": obj["queue"].title,
|
||||
"username": obj["user"].get_username(),
|
||||
}
|
||||
else:
|
||||
return _("Helpdesk: Open Tickets for %(username)s") % {
|
||||
'username': obj['user'].get_username(),
|
||||
"username": obj["user"].get_username(),
|
||||
}
|
||||
|
||||
def description(self, obj):
|
||||
if obj['queue']:
|
||||
return _("Open and Reopened Tickets in queue %(queue)s for %(username)s") % {
|
||||
'queue': obj['queue'].title,
|
||||
'username': obj['user'].get_username(),
|
||||
if obj["queue"]:
|
||||
return _(
|
||||
"Open and Reopened Tickets in queue %(queue)s for %(username)s"
|
||||
) % {
|
||||
"queue": obj["queue"].title,
|
||||
"username": obj["user"].get_username(),
|
||||
}
|
||||
else:
|
||||
return _("Open and Reopened Tickets for %(username)s") % {
|
||||
'username': obj['user'].get_username(),
|
||||
"username": obj["user"].get_username(),
|
||||
}
|
||||
|
||||
def link(self, obj):
|
||||
if obj['queue']:
|
||||
return u'%s?assigned_to=%s&queue=%s' % (
|
||||
reverse('helpdesk:list'),
|
||||
obj['user'].id,
|
||||
obj['queue'].id,
|
||||
if obj["queue"]:
|
||||
return "%s?assigned_to=%s&queue=%s" % (
|
||||
reverse("helpdesk:list"),
|
||||
obj["user"].id,
|
||||
obj["queue"].id,
|
||||
)
|
||||
else:
|
||||
return u'%s?assigned_to=%s' % (
|
||||
reverse('helpdesk:list'),
|
||||
obj['user'].id,
|
||||
return "%s?assigned_to=%s" % (
|
||||
reverse("helpdesk:list"),
|
||||
obj["user"].id,
|
||||
)
|
||||
|
||||
def items(self, obj):
|
||||
if obj['queue']:
|
||||
return Ticket.objects.filter(
|
||||
assigned_to=obj['user']
|
||||
).filter(
|
||||
queue=obj['queue']
|
||||
).filter(Q_OPEN_STATUSES)
|
||||
if obj["queue"]:
|
||||
return (
|
||||
Ticket.objects.filter(assigned_to=obj["user"])
|
||||
.filter(queue=obj["queue"])
|
||||
.filter(Q_OPEN_STATUSES)
|
||||
)
|
||||
else:
|
||||
return Ticket.objects.filter(
|
||||
assigned_to=obj['user']
|
||||
).filter(Q_OPEN_STATUSES)
|
||||
return Ticket.objects.filter(assigned_to=obj["user"]).filter(
|
||||
Q_OPEN_STATUSES
|
||||
)
|
||||
|
||||
def item_pubdate(self, item):
|
||||
return item.created
|
||||
@ -91,21 +93,19 @@ class OpenTicketsByUser(Feed):
|
||||
if item.assigned_to:
|
||||
return item.assigned_to.get_username()
|
||||
else:
|
||||
return _('Unassigned')
|
||||
return _("Unassigned")
|
||||
|
||||
|
||||
class UnassignedTickets(Feed):
|
||||
title_template = 'helpdesk/rss/ticket_title.html'
|
||||
description_template = 'helpdesk/rss/ticket_description.html'
|
||||
title_template = "helpdesk/rss/ticket_title.html"
|
||||
description_template = "helpdesk/rss/ticket_description.html"
|
||||
|
||||
title = _('Helpdesk: Unassigned Tickets')
|
||||
description = _('Unassigned Open and Reopened tickets')
|
||||
link = '' # '%s?assigned_to=' % reverse('helpdesk:list')
|
||||
title = _("Helpdesk: Unassigned Tickets")
|
||||
description = _("Unassigned Open and Reopened tickets")
|
||||
link = "" # '%s?assigned_to=' % reverse('helpdesk:list')
|
||||
|
||||
def items(self, obj):
|
||||
return Ticket.objects.filter(
|
||||
assigned_to__isnull=True
|
||||
).filter(Q_OPEN_STATUSES)
|
||||
return Ticket.objects.filter(assigned_to__isnull=True).filter(Q_OPEN_STATUSES)
|
||||
|
||||
def item_pubdate(self, item):
|
||||
return item.created
|
||||
@ -114,49 +114,48 @@ class UnassignedTickets(Feed):
|
||||
if item.assigned_to:
|
||||
return item.assigned_to.get_username()
|
||||
else:
|
||||
return _('Unassigned')
|
||||
return _("Unassigned")
|
||||
|
||||
|
||||
class RecentFollowUps(Feed):
|
||||
title_template = 'helpdesk/rss/recent_activity_title.html'
|
||||
description_template = 'helpdesk/rss/recent_activity_description.html'
|
||||
title_template = "helpdesk/rss/recent_activity_title.html"
|
||||
description_template = "helpdesk/rss/recent_activity_description.html"
|
||||
|
||||
title = _('Helpdesk: Recent Followups')
|
||||
title = _("Helpdesk: Recent Followups")
|
||||
description = _(
|
||||
'Recent FollowUps, such as e-mail replies, comments, attachments and resolutions')
|
||||
link = '/tickets/' # reverse('helpdesk:list')
|
||||
"Recent FollowUps, such as e-mail replies, comments, attachments and resolutions"
|
||||
)
|
||||
link = "/tickets/" # reverse('helpdesk:list')
|
||||
|
||||
def items(self):
|
||||
return FollowUp.objects.order_by('-date')[:20]
|
||||
return FollowUp.objects.order_by("-date")[:20]
|
||||
|
||||
|
||||
class OpenTicketsByQueue(Feed):
|
||||
title_template = 'helpdesk/rss/ticket_title.html'
|
||||
description_template = 'helpdesk/rss/ticket_description.html'
|
||||
title_template = "helpdesk/rss/ticket_title.html"
|
||||
description_template = "helpdesk/rss/ticket_description.html"
|
||||
|
||||
def get_object(self, request, queue_slug):
|
||||
return get_object_or_404(Queue, slug=queue_slug)
|
||||
|
||||
def title(self, obj):
|
||||
return _('Helpdesk: Open Tickets in queue %(queue)s') % {
|
||||
'queue': obj.title,
|
||||
return _("Helpdesk: Open Tickets in queue %(queue)s") % {
|
||||
"queue": obj.title,
|
||||
}
|
||||
|
||||
def description(self, obj):
|
||||
return _('Open and Reopened Tickets in queue %(queue)s') % {
|
||||
'queue': obj.title,
|
||||
return _("Open and Reopened Tickets in queue %(queue)s") % {
|
||||
"queue": obj.title,
|
||||
}
|
||||
|
||||
def link(self, obj):
|
||||
return '%s?queue=%s' % (
|
||||
reverse('helpdesk:list'),
|
||||
return "%s?queue=%s" % (
|
||||
reverse("helpdesk:list"),
|
||||
obj.id,
|
||||
)
|
||||
|
||||
def items(self, obj):
|
||||
return Ticket.objects.filter(
|
||||
queue=obj
|
||||
).filter(Q_OPEN_STATUSES)
|
||||
return Ticket.objects.filter(queue=obj).filter(Q_OPEN_STATUSES)
|
||||
|
||||
def item_pubdate(self, item):
|
||||
return item.created
|
||||
@ -165,4 +164,4 @@ class OpenTicketsByQueue(Feed):
|
||||
if item.assigned_to:
|
||||
return item.assigned_to.get_username()
|
||||
else:
|
||||
return _('Unassigned')
|
||||
return _("Unassigned")
|
||||
|
@ -18,10 +18,14 @@ from helpdesk.models import KBCategory, KBItem
|
||||
def index(request):
|
||||
huser = user.huser_from_request(request)
|
||||
# TODO: It'd be great to have a list of most popular items here.
|
||||
return render(request, 'helpdesk/kb_index.html', {
|
||||
'kb_categories': huser.get_allowed_kb_categories(),
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
})
|
||||
return render(
|
||||
request,
|
||||
"helpdesk/kb_index.html",
|
||||
{
|
||||
"kb_categories": huser.get_allowed_kb_categories(),
|
||||
"helpdesk_settings": helpdesk_settings,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def category(request, slug, iframe=False):
|
||||
@ -29,29 +33,33 @@ def category(request, slug, iframe=False):
|
||||
if not user.huser_from_request(request).can_access_kbcategory(category):
|
||||
raise Http404
|
||||
items = category.kbitem_set.filter(enabled=True)
|
||||
selected_item = request.GET.get('kbitem', None)
|
||||
selected_item = request.GET.get("kbitem", None)
|
||||
try:
|
||||
selected_item = int(selected_item)
|
||||
except TypeError:
|
||||
pass
|
||||
qparams = request.GET.copy()
|
||||
try:
|
||||
del qparams['kbitem']
|
||||
del qparams["kbitem"]
|
||||
except KeyError:
|
||||
pass
|
||||
template = 'helpdesk/kb_category.html'
|
||||
template = "helpdesk/kb_category.html"
|
||||
if iframe:
|
||||
template = 'helpdesk/kb_category_iframe.html'
|
||||
template = "helpdesk/kb_category_iframe.html"
|
||||
staff = request.user.is_authenticated and request.user.is_staff
|
||||
return render(request, template, {
|
||||
'category': category,
|
||||
'items': items,
|
||||
'selected_item': selected_item,
|
||||
'query_param_string': qparams.urlencode(),
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
'iframe': iframe,
|
||||
'staff': staff,
|
||||
})
|
||||
return render(
|
||||
request,
|
||||
template,
|
||||
{
|
||||
"category": category,
|
||||
"items": items,
|
||||
"selected_item": selected_item,
|
||||
"query_param_string": qparams.urlencode(),
|
||||
"helpdesk_settings": helpdesk_settings,
|
||||
"iframe": iframe,
|
||||
"staff": staff,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@xframe_options_exempt
|
||||
@ -62,7 +70,7 @@ def category_iframe(request, slug):
|
||||
def vote(request, item, vote):
|
||||
item = get_object_or_404(KBItem, pk=item)
|
||||
if request.method == "POST":
|
||||
if vote == 'up':
|
||||
if vote == "up":
|
||||
if not item.voted_by.filter(pk=request.user.pk):
|
||||
item.votes += 1
|
||||
item.voted_by.add(request.user.pk)
|
||||
@ -70,7 +78,7 @@ def vote(request, item, vote):
|
||||
if item.downvoted_by.filter(pk=request.user.pk):
|
||||
item.votes -= 1
|
||||
item.downvoted_by.remove(request.user.pk)
|
||||
if vote == 'down':
|
||||
if vote == "down":
|
||||
if not item.downvoted_by.filter(pk=request.user.pk):
|
||||
item.votes += 1
|
||||
item.downvoted_by.add(request.user.pk)
|
||||
|
@ -5,24 +5,22 @@ from django.shortcuts import resolve_url
|
||||
|
||||
|
||||
default_login_view = auth_views.LoginView.as_view(
|
||||
template_name='helpdesk/registration/login.html')
|
||||
template_name="helpdesk/registration/login.html"
|
||||
)
|
||||
|
||||
|
||||
def login(request):
|
||||
login_url = settings.LOGIN_URL
|
||||
# Prevent redirect loop by checking that LOGIN_URL is not this view's name
|
||||
condition = (
|
||||
login_url
|
||||
and (
|
||||
login_url != resolve_url(request.resolver_match.view_name)
|
||||
and (login_url != request.resolver_match.view_name)
|
||||
)
|
||||
condition = login_url and (
|
||||
login_url != resolve_url(request.resolver_match.view_name)
|
||||
and (login_url != request.resolver_match.view_name)
|
||||
)
|
||||
if condition:
|
||||
if 'next' in request.GET:
|
||||
return_to = request.GET['next']
|
||||
if "next" in request.GET:
|
||||
return_to = request.GET["next"]
|
||||
else:
|
||||
return_to = resolve_url('helpdesk:home')
|
||||
return_to = resolve_url("helpdesk:home")
|
||||
return redirect_to_login(return_to, login_url)
|
||||
else:
|
||||
return default_login_view(request)
|
||||
|
@ -7,9 +7,12 @@ views/public.py - All public facing views, eg non-staff (no authentication
|
||||
required) views.
|
||||
"""
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied
|
||||
from django.core.exceptions import (
|
||||
ImproperlyConfigured,
|
||||
ObjectDoesNotExist,
|
||||
PermissionDenied,
|
||||
)
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
@ -41,11 +44,11 @@ def create_ticket(request, *args, **kwargs):
|
||||
|
||||
|
||||
class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
|
||||
|
||||
def get_form_class(self):
|
||||
try:
|
||||
the_module, the_form_class = helpdesk_settings.HELPDESK_PUBLIC_TICKET_FORM_CLASS.rsplit(
|
||||
".", 1)
|
||||
the_module, the_form_class = (
|
||||
helpdesk_settings.HELPDESK_PUBLIC_TICKET_FORM_CLASS.rsplit(".", 1)
|
||||
)
|
||||
the_module = import_module(the_module)
|
||||
the_form_class = getattr(the_module, the_form_class)
|
||||
except Exception as e:
|
||||
@ -56,76 +59,85 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
request = self.request
|
||||
if not request.user.is_authenticated and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT:
|
||||
return HttpResponseRedirect(reverse('login'))
|
||||
if (
|
||||
not request.user.is_authenticated
|
||||
and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT
|
||||
):
|
||||
return HttpResponseRedirect(reverse("login"))
|
||||
|
||||
if is_helpdesk_staff(request.user) or \
|
||||
(request.user.is_authenticated and
|
||||
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE):
|
||||
if is_helpdesk_staff(request.user) or (
|
||||
request.user.is_authenticated
|
||||
and helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
||||
):
|
||||
try:
|
||||
if request.user.usersettings_helpdesk.login_view_ticketlist:
|
||||
return HttpResponseRedirect(reverse('helpdesk:list'))
|
||||
return HttpResponseRedirect(reverse("helpdesk:list"))
|
||||
else:
|
||||
return HttpResponseRedirect(reverse('helpdesk:dashboard'))
|
||||
return HttpResponseRedirect(reverse("helpdesk:dashboard"))
|
||||
except UserSettings.DoesNotExist:
|
||||
return HttpResponseRedirect(reverse('helpdesk:dashboard'))
|
||||
return HttpResponseRedirect(reverse("helpdesk:dashboard"))
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
def get_initial(self):
|
||||
initial_data = super().get_initial()
|
||||
|
||||
# add pre-defined data for public ticket
|
||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE'):
|
||||
if hasattr(settings, "HELPDESK_PUBLIC_TICKET_QUEUE"):
|
||||
# get the requested queue; return an error if queue not found
|
||||
try:
|
||||
initial_data['queue'] = Queue.objects.get(
|
||||
initial_data["queue"] = Queue.objects.get(
|
||||
slug=settings.HELPDESK_PUBLIC_TICKET_QUEUE,
|
||||
allow_public_submission=True
|
||||
allow_public_submission=True,
|
||||
).id
|
||||
except Queue.DoesNotExist as e:
|
||||
logger.fatal(
|
||||
"Public queue '%s' is configured as default but can't be found",
|
||||
settings.HELPDESK_PUBLIC_TICKET_QUEUE
|
||||
settings.HELPDESK_PUBLIC_TICKET_QUEUE,
|
||||
)
|
||||
raise ImproperlyConfigured(
|
||||
"Wrong public queue configuration") from e
|
||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'):
|
||||
initial_data['priority'] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY
|
||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_DUE_DATE'):
|
||||
initial_data['due_date'] = settings.HELPDESK_PUBLIC_TICKET_DUE_DATE
|
||||
raise ImproperlyConfigured("Wrong public queue configuration") from e
|
||||
if hasattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY"):
|
||||
initial_data["priority"] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY
|
||||
if hasattr(settings, "HELPDESK_PUBLIC_TICKET_DUE_DATE"):
|
||||
initial_data["due_date"] = settings.HELPDESK_PUBLIC_TICKET_DUE_DATE
|
||||
return initial_data
|
||||
|
||||
def get_form_kwargs(self, *args, **kwargs):
|
||||
kwargs = super().get_form_kwargs(*args, **kwargs)
|
||||
if '_hide_fields_' in self.request.GET:
|
||||
kwargs['hidden_fields'] = self.request.GET.get(
|
||||
'_hide_fields_', '').split(',')
|
||||
kwargs['readonly_fields'] = self.request.GET.get(
|
||||
'_readonly_fields_', '').split(',')
|
||||
if "_hide_fields_" in self.request.GET:
|
||||
kwargs["hidden_fields"] = self.request.GET.get("_hide_fields_", "").split(
|
||||
","
|
||||
)
|
||||
kwargs["readonly_fields"] = self.request.GET.get("_readonly_fields_", "").split(
|
||||
","
|
||||
)
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
request = self.request
|
||||
if text_is_spam(form.cleaned_data['body'], request):
|
||||
if text_is_spam(form.cleaned_data["body"], request):
|
||||
# This submission is spam. Let's not save it.
|
||||
return render(request, template_name='helpdesk/public_spam.html')
|
||||
return render(request, template_name="helpdesk/public_spam.html")
|
||||
else:
|
||||
ticket = form.save(
|
||||
user=self.request.user if self.request.user.is_authenticated else None)
|
||||
user=self.request.user if self.request.user.is_authenticated else None
|
||||
)
|
||||
try:
|
||||
return HttpResponseRedirect('%s?ticket=%s&email=%s&key=%s' % (
|
||||
reverse('helpdesk:public_view'),
|
||||
ticket.ticket_for_url,
|
||||
quote(ticket.submitter_email),
|
||||
ticket.secret_key)
|
||||
return HttpResponseRedirect(
|
||||
"%s?ticket=%s&email=%s&key=%s"
|
||||
% (
|
||||
reverse("helpdesk:public_view"),
|
||||
ticket.ticket_for_url,
|
||||
quote(ticket.submitter_email),
|
||||
ticket.secret_key,
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
# if someone enters a non-int string for the ticket
|
||||
return HttpResponseRedirect(reverse('helpdesk:home'))
|
||||
return HttpResponseRedirect(reverse("helpdesk:home"))
|
||||
|
||||
|
||||
class CreateTicketIframeView(BaseCreateTicketView):
|
||||
template_name = 'helpdesk/public_create_ticket_iframe.html'
|
||||
template_name = "helpdesk/public_create_ticket_iframe.html"
|
||||
|
||||
@csrf_exempt
|
||||
@xframe_options_exempt
|
||||
@ -134,11 +146,11 @@ class CreateTicketIframeView(BaseCreateTicketView):
|
||||
|
||||
def form_valid(self, form):
|
||||
if super().form_valid(form).status_code == 302:
|
||||
return HttpResponseRedirect(reverse('helpdesk:success_iframe'))
|
||||
return HttpResponseRedirect(reverse("helpdesk:success_iframe"))
|
||||
|
||||
|
||||
class SuccessIframeView(TemplateView):
|
||||
template_name = 'helpdesk/success_iframe.html'
|
||||
template_name = "helpdesk/success_iframe.html"
|
||||
|
||||
@xframe_options_exempt
|
||||
def dispatch(self, *args, **kwargs):
|
||||
@ -146,123 +158,140 @@ class SuccessIframeView(TemplateView):
|
||||
|
||||
|
||||
class CreateTicketView(BaseCreateTicketView):
|
||||
template_name = 'helpdesk/public_create_ticket.html'
|
||||
template_name = "helpdesk/public_create_ticket.html"
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
# Add the CSS error class to the form in order to better see them in
|
||||
# the page
|
||||
form.error_css_class = 'text-danger'
|
||||
form.error_css_class = "text-danger"
|
||||
return form
|
||||
|
||||
|
||||
class Homepage(CreateTicketView):
|
||||
template_name = 'helpdesk/public_homepage.html'
|
||||
template_name = "helpdesk/public_homepage.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['kb_categories'] = huser_from_request(
|
||||
self.request).get_allowed_kb_categories()
|
||||
context["kb_categories"] = huser_from_request(
|
||||
self.request
|
||||
).get_allowed_kb_categories()
|
||||
return context
|
||||
|
||||
|
||||
class SearchForTicketView(TemplateView):
|
||||
template_name = 'helpdesk/public_view_form.html'
|
||||
template_name = "helpdesk/public_view_form.html"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC:
|
||||
if (
|
||||
hasattr(settings, "HELPDESK_VIEW_A_TICKET_PUBLIC")
|
||||
and settings.HELPDESK_VIEW_A_TICKET_PUBLIC
|
||||
):
|
||||
context = self.get_context_data(**kwargs)
|
||||
return self.render_to_response(context)
|
||||
else:
|
||||
raise PermissionDenied("Public viewing of tickets without a secret key is forbidden.")
|
||||
raise PermissionDenied(
|
||||
"Public viewing of tickets without a secret key is forbidden."
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
request = self.request
|
||||
email = request.GET.get('email', None)
|
||||
error_message = kwargs.get('error_message', None)
|
||||
email = request.GET.get("email", None)
|
||||
error_message = kwargs.get("error_message", None)
|
||||
|
||||
context.update({
|
||||
'ticket': False,
|
||||
'email': email,
|
||||
'error_message': error_message,
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
})
|
||||
context.update(
|
||||
{
|
||||
"ticket": False,
|
||||
"email": email,
|
||||
"error_message": error_message,
|
||||
"helpdesk_settings": helpdesk_settings,
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class ViewTicket(TemplateView):
|
||||
template_name = 'helpdesk/public_view_ticket.html'
|
||||
|
||||
template_name = "helpdesk/public_view_ticket.html"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
ticket_req = request.GET.get('ticket', None)
|
||||
email = request.GET.get('email', None)
|
||||
key = request.GET.get('key', '')
|
||||
ticket_req = request.GET.get("ticket", None)
|
||||
email = request.GET.get("email", None)
|
||||
key = request.GET.get("key", "")
|
||||
|
||||
if not (ticket_req and email):
|
||||
if ticket_req is None and email is None:
|
||||
return SearchForTicketView.as_view()(request)
|
||||
else:
|
||||
return SearchForTicketView.as_view()(request, _('Missing ticket ID or e-mail address. Please try again.'))
|
||||
return SearchForTicketView.as_view()(
|
||||
request, _("Missing ticket ID or e-mail address. Please try again.")
|
||||
)
|
||||
|
||||
try:
|
||||
queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req)
|
||||
if request.user.is_authenticated and request.user.email == email:
|
||||
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email)
|
||||
elif hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC:
|
||||
elif (
|
||||
hasattr(settings, "HELPDESK_VIEW_A_TICKET_PUBLIC")
|
||||
and settings.HELPDESK_VIEW_A_TICKET_PUBLIC
|
||||
):
|
||||
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email)
|
||||
else:
|
||||
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email, secret_key__iexact=key)
|
||||
ticket = Ticket.objects.get(
|
||||
id=ticket_id, submitter_email__iexact=email, secret_key__iexact=key
|
||||
)
|
||||
except (ObjectDoesNotExist, ValueError):
|
||||
return SearchForTicketView.as_view()(request, _('Invalid ticket ID or e-mail address. Please try again.'))
|
||||
return SearchForTicketView.as_view()(
|
||||
request, _("Invalid ticket ID or e-mail address. Please try again.")
|
||||
)
|
||||
|
||||
if 'close' in request.GET and ticket.status == Ticket.RESOLVED_STATUS:
|
||||
if "close" in request.GET and ticket.status == Ticket.RESOLVED_STATUS:
|
||||
from helpdesk.update_ticket import update_ticket
|
||||
|
||||
update_ticket(
|
||||
request.user,
|
||||
ticket,
|
||||
public=True,
|
||||
comment=_('Submitter accepted resolution and closed ticket'),
|
||||
comment=_("Submitter accepted resolution and closed ticket"),
|
||||
new_status=Ticket.CLOSED_STATUS,
|
||||
)
|
||||
return HttpResponseRedirect(ticket.ticket_url)
|
||||
|
||||
# Prepare context for rendering
|
||||
context = {
|
||||
'key': key,
|
||||
'mail': email,
|
||||
'ticket': ticket,
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
'next': self.get_next_url(ticket_id)
|
||||
"key": key,
|
||||
"mail": email,
|
||||
"ticket": ticket,
|
||||
"helpdesk_settings": helpdesk_settings,
|
||||
"next": self.get_next_url(ticket_id),
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
||||
def get_next_url(self, ticket_id):
|
||||
redirect_url = ''
|
||||
redirect_url = ""
|
||||
if is_helpdesk_staff(self.request.user):
|
||||
redirect_url = reverse('helpdesk:view', args=[ticket_id])
|
||||
if 'close' in self.request.GET:
|
||||
redirect_url += '?close'
|
||||
redirect_url = reverse("helpdesk:view", args=[ticket_id])
|
||||
if "close" in self.request.GET:
|
||||
redirect_url += "?close"
|
||||
elif helpdesk_settings.HELPDESK_NAVIGATION_ENABLED:
|
||||
redirect_url = reverse('helpdesk:view', args=[ticket_id])
|
||||
redirect_url = reverse("helpdesk:view", args=[ticket_id])
|
||||
return redirect_url
|
||||
|
||||
|
||||
class MyTickets(TemplateView):
|
||||
template_name = 'helpdesk/my_tickets.html'
|
||||
template_name = "helpdesk/my_tickets.html"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponseRedirect(reverse('helpdesk:login'))
|
||||
return HttpResponseRedirect(reverse("helpdesk:login"))
|
||||
|
||||
context = self.get_context_data(**kwargs)
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
||||
def change_language(request):
|
||||
return_to = ''
|
||||
if 'return_to' in request.GET:
|
||||
return_to = request.GET['return_to']
|
||||
return_to = ""
|
||||
if "return_to" in request.GET:
|
||||
return_to = request.GET["return_to"]
|
||||
|
||||
return render(request, 'helpdesk/public_change_language.html', {'next': return_to})
|
||||
return render(request, "helpdesk/public_change_language.html", {"next": return_to})
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@ from .signals import new_ticket_done, update_ticket_done
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def notify_followup_webhooks(followup):
|
||||
urls = settings.HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS()
|
||||
if not urls:
|
||||
@ -15,22 +16,24 @@ def notify_followup_webhooks(followup):
|
||||
|
||||
# Serialize the ticket associated with the followup
|
||||
from .serializers import TicketSerializer
|
||||
|
||||
ticket = followup.ticket
|
||||
ticket.set_custom_field_values()
|
||||
serialized_ticket = TicketSerializer(ticket).data
|
||||
|
||||
# Prepare the data to send
|
||||
data = {
|
||||
'ticket': serialized_ticket,
|
||||
'queue_slug': ticket.queue.slug,
|
||||
'followup_id': followup.id
|
||||
"ticket": serialized_ticket,
|
||||
"queue_slug": ticket.queue.slug,
|
||||
"followup_id": followup.id,
|
||||
}
|
||||
|
||||
for url in urls:
|
||||
try:
|
||||
requests.post(url, json=data, timeout=settings.HELPDESK_WEBHOOK_TIMEOUT)
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error('Timeout while sending followup webhook to %s', url)
|
||||
logger.error("Timeout while sending followup webhook to %s", url)
|
||||
|
||||
|
||||
# listener is loaded via app.py HelpdeskConfig.ready()
|
||||
@receiver(update_ticket_done)
|
||||
@ -44,22 +47,21 @@ def send_new_ticket_webhook(ticket):
|
||||
return
|
||||
# Serialize the ticket
|
||||
from .serializers import TicketSerializer
|
||||
|
||||
ticket.set_custom_field_values()
|
||||
serialized_ticket = TicketSerializer(ticket).data
|
||||
|
||||
# Prepare the data to send
|
||||
data = {
|
||||
'ticket': serialized_ticket,
|
||||
'queue_slug': ticket.queue.slug
|
||||
}
|
||||
data = {"ticket": serialized_ticket, "queue_slug": ticket.queue.slug}
|
||||
|
||||
for url in urls:
|
||||
try:
|
||||
requests.post(url, json=data, timeout=settings.HELPDESK_WEBHOOK_TIMEOUT)
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error('Timeout while sending new ticket webhook to %s', url)
|
||||
logger.error("Timeout while sending new ticket webhook to %s", url)
|
||||
|
||||
|
||||
# listener is loaded via app.py HelpdeskConfig.ready()
|
||||
@receiver(new_ticket_done)
|
||||
def send_new_ticket_webhook_receiver(sender, ticket, **kwargs):
|
||||
send_new_ticket_webhook(ticket)
|
||||
send_new_ticket_webhook(ticket)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user