Merge pull request #1249 from DavidVadnais/BUG-1187-Fix-make-check-format

Bug 1187 fix make check format
This commit is contained in:
Christopher Broderick 2025-03-24 18:16:17 +00:00 committed by GitHub
commit f3cfd0ec4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
101 changed files with 7664 additions and 4982 deletions

View File

@ -9,9 +9,7 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
# Explicitly include Python 3.8 and 3.9 only with Django 4 # Explicitly include Python 3.9 only with Django 4
- python-version: "3.8"
django-version: "4"
- python-version: "3.9" - python-version: "3.9"
django-version: "4" django-version: "4"
# Define the general matrix for Python with Django # Define the general matrix for Python with Django
@ -38,7 +36,8 @@ jobs:
- name: Lint with ruff - name: Lint with ruff
run: | run: |
pip install ruff pip install ruff
ruff check helpdesk make checkformat
- name: Test with pytest - name: Test with pytest
run: | run: |

View File

@ -1,5 +1,5 @@
Copyright (c) 2008 Ross Poulton (Trading as Jutda), 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. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,

View File

@ -63,15 +63,13 @@ test:
#: format - Run the PEP8 formatter. #: format - Run the PEP8 formatter.
.PHONY: format .PHONY: format
format: format:
autopep8 --exit-code --global-config .flake8 helpdesk ruff format helpdesk
isort --line-length=120 --src helpdesk .
#: checkformat - checks formatting against configured format specifications for the project. #: checkformat - checks formatting against configured format specifications for the project.
.PHONY: checkformat .PHONY: checkformat
checkformat: checkformat:
flake8 helpdesk --count --show-source --statistics --exit-zero --max-complexity=20 ruff check helpdesk
isort --line-length=120 --src helpdesk . --check
#: documentation - Build documentation (Sphinx, README, ...). #: documentation - Build documentation (Sphinx, README, ...).

View File

@ -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 .. image:: https://codecov.io/gh/django-helpdesk/django-helpdesk/branch/develop/graph/badge.svg
:target: https://codecov.io/gh/django-helpdesk/django-helpdesk :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. See LICENSE for details.
django-helpdesk was formerly known as Jutda Helpdesk, named after the django-helpdesk was formerly known as Jutda Helpdesk, named after the

View File

@ -1,4 +1,3 @@
from django.contrib import admin from django.contrib import admin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
@ -16,7 +15,7 @@ from helpdesk.models import (
PreSetReply, PreSetReply,
Queue, Queue,
Ticket, Ticket,
TicketChange TicketChange,
) )
@ -26,7 +25,7 @@ if helpdesk_settings.HELPDESK_KB_ENABLED:
@admin.register(Queue) @admin.register(Queue)
class QueueAdmin(admin.ModelAdmin): 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",)} prepopulated_fields = {"slug": ("title",)}
def time_spent(self, q): def time_spent(self, q):
@ -44,11 +43,17 @@ class QueueAdmin(admin.ModelAdmin):
@admin.register(Ticket) @admin.register(Ticket)
class TicketAdmin(admin.ModelAdmin): class TicketAdmin(admin.ModelAdmin):
list_display = ('title', 'status', 'assigned_to', 'queue', list_display = (
'hidden_submitter_email', 'time_spent') "title",
date_hierarchy = 'created' "status",
list_filter = ('queue', 'assigned_to', 'status') "assigned_to",
search_fields = ('id', 'title') "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): def hidden_submitter_email(self, ticket):
if ticket.submitter_email: if ticket.submitter_email:
@ -58,7 +63,8 @@ class TicketAdmin(admin.ModelAdmin):
return "%s@%s" % (username, domain) return "%s@%s" % (username, domain)
else: else:
return ticket.submitter_email 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): def time_spent(self, ticket):
return ticket.time_spent return ticket.time_spent
@ -82,51 +88,60 @@ class KBIAttachmentInline(admin.StackedInline):
@admin.register(FollowUp) @admin.register(FollowUp)
class FollowUpAdmin(admin.ModelAdmin): class FollowUpAdmin(admin.ModelAdmin):
inlines = [TicketChangeInline, FollowUpAttachmentInline] inlines = [TicketChangeInline, FollowUpAttachmentInline]
list_display = ('ticket_get_ticket_for_url', 'title', 'date', 'ticket', list_display = (
'user', 'new_status', 'time_spent') "ticket_get_ticket_for_url",
list_filter = ('user', 'date', 'new_status') "title",
"date",
"ticket",
"user",
"new_status",
"time_spent",
)
list_filter = ("user", "date", "new_status")
def ticket_get_ticket_for_url(self, obj): def ticket_get_ticket_for_url(self, obj):
return obj.ticket.ticket_for_url 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: if helpdesk_settings.HELPDESK_KB_ENABLED:
@admin.register(KBItem) @admin.register(KBItem)
class KBItemAdmin(admin.ModelAdmin): class KBItemAdmin(admin.ModelAdmin):
list_display = ('category', 'title', 'last_updated', list_display = ("category", "title", "last_updated", "team", "order", "enabled")
'team', 'order', 'enabled')
inlines = [KBIAttachmentInline] 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: if helpdesk_settings.HELPDESK_KB_ENABLED:
@admin.register(KBCategory) @admin.register(KBCategory)
class KBCategoryAdmin(admin.ModelAdmin): class KBCategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'title', 'slug', 'public') list_display = ("name", "title", "slug", "public")
@admin.register(CustomField) @admin.register(CustomField)
class CustomFieldAdmin(admin.ModelAdmin): class CustomFieldAdmin(admin.ModelAdmin):
list_display = ('name', 'label', 'data_type') list_display = ("name", "label", "data_type")
@admin.register(EmailTemplate) @admin.register(EmailTemplate)
class EmailTemplateAdmin(admin.ModelAdmin): class EmailTemplateAdmin(admin.ModelAdmin):
list_display = ('template_name', 'heading', 'locale') list_display = ("template_name", "heading", "locale")
list_filter = ('locale', ) list_filter = ("locale",)
@admin.register(IgnoreEmail) @admin.register(IgnoreEmail)
class IgnoreEmailAdmin(admin.ModelAdmin): 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) @admin.register(ChecklistTemplate)
class ChecklistTemplateAdmin(admin.ModelAdmin): class ChecklistTemplateAdmin(admin.ModelAdmin):
list_display = ('name', 'task_list') list_display = ("name", "task_list")
search_fields = ('name', 'task_list') search_fields = ("name", "task_list")
class ChecklistTaskInline(admin.TabularInline): class ChecklistTaskInline(admin.TabularInline):
@ -135,10 +150,10 @@ class ChecklistTaskInline(admin.TabularInline):
@admin.register(Checklist) @admin.register(Checklist)
class ChecklistAdmin(admin.ModelAdmin): class ChecklistAdmin(admin.ModelAdmin):
list_display = ('name', 'ticket') list_display = ("name", "ticket")
search_fields = ('name', 'ticket__id', 'ticket__title') search_fields = ("name", "ticket__id", "ticket__title")
autocomplete_fields = ('ticket',) autocomplete_fields = ("ticket",)
list_select_related = ('ticket',) list_select_related = ("ticket",)
inlines = (ChecklistTaskInline,) inlines = (ChecklistTaskInline,)

View File

@ -2,12 +2,12 @@ from django.apps import AppConfig
class HelpdeskConfig(AppConfig): class HelpdeskConfig(AppConfig):
name = 'helpdesk' name = "helpdesk"
verbose_name = "Helpdesk" verbose_name = "Helpdesk"
# for Django 3.2 support: # for Django 3.2 support:
# see: # see:
# https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.default_auto_field # 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): def ready(self):
from . import webhooks # noqa: F401 from . import webhooks # noqa: F401

View File

@ -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 function most only take one User parameter at the end for use with
the Django function user_passes_test. the Django function user_passes_test.
""" """
def check_superuser_status(check_superuser): def check_superuser_status(check_superuser):
def check_user_status(u): def check_user_status(u):
is_ok = u.is_authenticated and u.is_active 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 return is_ok and u.is_superuser
else: else:
return is_ok return is_ok
return check_user_status return check_user_status
return check_superuser_status return check_superuser_status
@ -43,11 +46,18 @@ def protect_view(view_func):
Decorator for protecting the views checking user, redirecting Decorator for protecting the views checking user, redirecting
to the log-in page if necessary or returning 404 status code to the log-in page if necessary or returning 404 status code
""" """
@wraps(view_func) @wraps(view_func)
def _wrapped_view(request, *args, **kwargs): def _wrapped_view(request, *args, **kwargs):
if not request.user.is_authenticated and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT: if (
return redirect('helpdesk:login') not request.user.is_authenticated
elif not request.user.is_authenticated and helpdesk_settings.HELPDESK_ANON_ACCESS_RAISES_404: 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 raise Http404
if auth_redirect := helpdesk_settings.HELPDESK_PUBLIC_VIEW_PROTECTOR(request): if auth_redirect := helpdesk_settings.HELPDESK_PUBLIC_VIEW_PROTECTOR(request):
return auth_redirect return auth_redirect
@ -61,11 +71,15 @@ def staff_member_required(view_func):
Decorator for staff member the views checking user, redirecting Decorator for staff member the views checking user, redirecting
to the log-in page if necessary or returning 403 to the log-in page if necessary or returning 403
""" """
@wraps(view_func) @wraps(view_func)
def _wrapped_view(request, *args, **kwargs): def _wrapped_view(request, *args, **kwargs):
if not request.user.is_authenticated and not request.user.is_active: if not request.user.is_authenticated and not request.user.is_active:
return redirect('helpdesk:login') return redirect("helpdesk:login")
if not helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE and not request.user.is_staff: if (
not helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
and not request.user.is_staff
):
raise PermissionDenied() raise PermissionDenied()
if auth_redirect := helpdesk_settings.HELPDESK_STAFF_VIEW_PROTECTOR(request): if auth_redirect := helpdesk_settings.HELPDESK_STAFF_VIEW_PROTECTOR(request):
return auth_redirect return auth_redirect
@ -79,10 +93,11 @@ def superuser_required(view_func):
Decorator for superuser member the views checking user, redirecting Decorator for superuser member the views checking user, redirecting
to the log-in page if necessary or returning 403 to the log-in page if necessary or returning 403
""" """
@wraps(view_func) @wraps(view_func)
def _wrapped_view(request, *args, **kwargs): def _wrapped_view(request, *args, **kwargs):
if not request.user.is_authenticated and not request.user.is_active: 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: if not request.user.is_superuser:
raise PermissionDenied() raise PermissionDenied()
return view_func(request, *args, **kwargs) return view_func(request, *args, **kwargs)

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@ class IgnoreTicketException(Exception):
""" """
Raised when an email message is received from a sender who is marked to be ignored Raised when an email message is received from a sender who is marked to be ignored
""" """
pass pass
@ -10,4 +11,5 @@ class DeleteIgnoredTicketException(Exception):
Raised when an email message is received from a sender who is marked to be ignored 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 and the record is tagged to delete the email from the inbox
""" """
pass pass

View File

@ -27,7 +27,7 @@ from helpdesk.models import (
TicketCC, TicketCC,
TicketCustomFieldValue, TicketCustomFieldValue,
TicketDependency, TicketDependency,
UserSettings UserSettings,
) )
from helpdesk.settings import ( from helpdesk.settings import (
CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATE_FORMAT,
@ -55,67 +55,71 @@ class CustomFieldMixin(object):
def customfield_to_field(self, field, instanceargs): def customfield_to_field(self, field, instanceargs):
# Use TextInput widget by default # Use TextInput widget by default
instanceargs['widget'] = forms.TextInput( instanceargs["widget"] = forms.TextInput(attrs={"class": "form-control"})
attrs={'class': 'form-control'})
# if-elif branches start with special cases # if-elif branches start with special cases
if field.data_type == 'varchar': if field.data_type == "varchar":
fieldclass = forms.CharField fieldclass = forms.CharField
instanceargs['max_length'] = field.max_length instanceargs["max_length"] = field.max_length
elif field.data_type == 'text': elif field.data_type == "text":
fieldclass = forms.CharField fieldclass = forms.CharField
instanceargs['widget'] = forms.Textarea( instanceargs["widget"] = forms.Textarea(attrs={"class": "form-control"})
attrs={'class': 'form-control'}) instanceargs["max_length"] = field.max_length
instanceargs['max_length'] = field.max_length elif field.data_type == "integer":
elif field.data_type == 'integer':
fieldclass = forms.IntegerField fieldclass = forms.IntegerField
instanceargs['widget'] = forms.NumberInput( instanceargs["widget"] = forms.NumberInput(attrs={"class": "form-control"})
attrs={'class': 'form-control'}) elif field.data_type == "decimal":
elif field.data_type == 'decimal':
fieldclass = forms.DecimalField fieldclass = forms.DecimalField
instanceargs['decimal_places'] = field.decimal_places instanceargs["decimal_places"] = field.decimal_places
instanceargs['max_digits'] = field.max_length instanceargs["max_digits"] = field.max_length
instanceargs['widget'] = forms.NumberInput( instanceargs["widget"] = forms.NumberInput(attrs={"class": "form-control"})
attrs={'class': 'form-control'}) elif field.data_type == "list":
elif field.data_type == 'list':
fieldclass = forms.ChoiceField fieldclass = forms.ChoiceField
instanceargs['choices'] = field.get_choices() instanceargs["choices"] = field.get_choices()
instanceargs['widget'] = forms.Select( instanceargs["widget"] = forms.Select(attrs={"class": "form-control"})
attrs={'class': 'form-control'})
else: else:
# Try to use the immediate equivalences dictionary # Try to use the immediate equivalences dictionary
try: try:
fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type] fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type]
# Change widgets for the following classes # Change widgets for the following classes
if fieldclass == forms.DateField: if fieldclass == forms.DateField:
instanceargs['widget'] = forms.DateInput( instanceargs["widget"] = forms.DateInput(
attrs={'class': 'form-control date-field'}) attrs={"class": "form-control date-field"}
)
elif fieldclass == forms.DateTimeField: elif fieldclass == forms.DateTimeField:
instanceargs['widget'] = forms.DateTimeInput( instanceargs["widget"] = forms.DateTimeInput(
attrs={'class': 'form-control datetime-field'}) attrs={"class": "form-control datetime-field"}
)
elif fieldclass == forms.TimeField: elif fieldclass == forms.TimeField:
instanceargs['widget'] = forms.TimeInput( instanceargs["widget"] = forms.TimeInput(
attrs={'class': 'form-control time-field'}) attrs={"class": "form-control time-field"}
)
elif fieldclass == forms.BooleanField: elif fieldclass == forms.BooleanField:
instanceargs['widget'] = forms.CheckboxInput( instanceargs["widget"] = forms.CheckboxInput(
attrs={'class': 'form-control'}) attrs={"class": "form-control"}
)
except KeyError: except KeyError:
# The data_type was not found anywhere # The data_type was not found anywhere
raise NameError("Unrecognized data_type %s" % field.data_type) 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 EditTicketForm(CustomFieldMixin, forms.ModelForm):
class Meta: class Meta:
model = Ticket model = Ticket
exclude = ('created', 'modified', 'status', 'on_hold', exclude = (
'resolution', 'last_escalation', 'assigned_to') "created",
"modified",
"status",
"on_hold",
"resolution",
"last_escalation",
"assigned_to",
)
class Media: class Media:
js = ('helpdesk/js/init_due_date.js', js = ("helpdesk/js/init_due_date.js", "helpdesk/js/init_datetime_classes.js")
'helpdesk/js/init_datetime_classes.js')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
""" """
@ -124,56 +128,62 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
super(EditTicketForm, self).__init__(*args, **kwargs) super(EditTicketForm, self).__init__(*args, **kwargs)
# Disable and add help_text to the merged_to field on this form # Disable and add help_text to the merged_to field on this form
self.fields['merged_to'].disabled = True self.fields["merged_to"].disabled = True
self.fields['merged_to'].help_text = _( self.fields["merged_to"].help_text = _(
'This ticket is merged into the selected ticket.') "This ticket is merged into the selected ticket."
)
for field in CustomField.objects.all(): for field in CustomField.objects.all():
initial_value = None initial_value = None
try: try:
current_value = TicketCustomFieldValue.objects.get( current_value = TicketCustomFieldValue.objects.get(
ticket=self.instance, field=field) ticket=self.instance, field=field
)
initial_value = current_value.value initial_value = current_value.value
# Attempt to convert from fixed format string to date/time data # Attempt to convert from fixed format string to date/time data
# type # type
if 'datetime' == current_value.field.data_type: if "datetime" == current_value.field.data_type:
initial_value = datetime.strptime( initial_value = datetime.strptime(
initial_value, CUSTOMFIELD_DATETIME_FORMAT) initial_value, CUSTOMFIELD_DATETIME_FORMAT
elif 'date' == current_value.field.data_type: )
elif "date" == current_value.field.data_type:
initial_value = datetime.strptime( initial_value = datetime.strptime(
initial_value, CUSTOMFIELD_DATE_FORMAT) initial_value, CUSTOMFIELD_DATE_FORMAT
elif 'time' == current_value.field.data_type: )
elif "time" == current_value.field.data_type:
initial_value = datetime.strptime( 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 # If it is boolean field, transform the value to a real boolean
# instead of a string # instead of a string
elif 'boolean' == current_value.field.data_type: elif "boolean" == current_value.field.data_type:
initial_value = 'True' == initial_value initial_value = "True" == initial_value
except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError): except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError):
# ValueError error if parsing fails, using initial_value = current_value.value # ValueError error if parsing fails, using initial_value = current_value.value
# TypeError if parsing None type # TypeError if parsing None type
pass pass
instanceargs = { instanceargs = {
'label': field.label, "label": field.label,
'help_text': field.help_text, "help_text": field.help_text,
'required': field.required, "required": field.required,
'initial': initial_value, "initial": initial_value,
} }
self.customfield_to_field(field, instanceargs) self.customfield_to_field(field, instanceargs)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
for field, value in self.cleaned_data.items(): for field, value in self.cleaned_data.items():
if field.startswith('custom_'): if field.startswith("custom_"):
field_name = field.replace('custom_', '', 1) field_name = field.replace("custom_", "", 1)
customfield = CustomField.objects.get(name=field_name) customfield = CustomField.objects.get(name=field_name)
try: try:
cfv = TicketCustomFieldValue.objects.get( cfv = TicketCustomFieldValue.objects.get(
ticket=self.instance, field=customfield) ticket=self.instance, field=customfield
)
except ObjectDoesNotExist: except ObjectDoesNotExist:
cfv = TicketCustomFieldValue( cfv = TicketCustomFieldValue(
ticket=self.instance, field=customfield) ticket=self.instance, field=customfield
)
cfv.value = convert_value(value) cfv.value = convert_value(value)
cfv.save() cfv.save()
@ -195,21 +205,24 @@ class EditTicketCustomFieldForm(EditTicketForm):
if HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST: if HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST:
fields = list(self.fields) fields = list(self.fields)
for field in 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) 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 # if form is saved in a ticket update, it is passed
# a followup instance to trace custom fields changes # a followup instance to trace custom fields changes
if "followup" in kwargs: if "followup" in kwargs:
followup = kwargs.pop('followup', None) followup = kwargs.pop("followup", None)
for field, value in self.cleaned_data.items(): for field, value in self.cleaned_data.items():
if field.startswith('custom_'): if field.startswith("custom_"):
if value != self.fields[field].initial: if value != self.fields[field].initial:
c = followup.ticketchange_set.create( c = followup.ticketchange_set.create(
field=field.replace('custom_', '', 1), field=field.replace("custom_", "", 1),
old_value=self.fields[field].initial, old_value=self.fields[field].initial,
new_value=value, new_value=value,
) )
@ -218,20 +231,26 @@ class EditTicketCustomFieldForm(EditTicketForm):
class Meta: class Meta:
model = Ticket model = Ticket
fields = ('id', 'merged_to',) fields = (
"id",
"merged_to",
)
class EditFollowUpForm(forms.ModelForm): class EditFollowUpForm(forms.ModelForm):
class Meta: class Meta:
model = FollowUp model = FollowUp
exclude = ('date', 'user',) exclude = (
"date",
"user",
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Filter not opened tickets here.""" """Filter not opened tickets here."""
super(EditFollowUpForm, self).__init__(*args, **kwargs) super(EditFollowUpForm, self).__init__(*args, **kwargs)
self.fields["ticket"].queryset = Ticket.objects.filter( self.fields["ticket"].queryset = Ticket.objects.filter(
status__in=Ticket.OPEN_STATUSES) status__in=Ticket.OPEN_STATUSES
)
class AbstractTicketForm(CustomFieldMixin, forms.Form): class AbstractTicketForm(CustomFieldMixin, forms.Form):
@ -239,73 +258,81 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
Contain all the common code and fields between "TicketForm" and Contain all the common code and fields between "TicketForm" and
"PublicTicketForm". This Form is not intended to be used directly. "PublicTicketForm". This Form is not intended to be used directly.
""" """
queue = forms.ChoiceField( queue = forms.ChoiceField(
widget=forms.Select(attrs={'class': 'form-control'}), widget=forms.Select(attrs={"class": "form-control"}),
label=_('Queue'), label=_("Queue"),
required=True, required=True,
choices=() choices=(),
) )
title = forms.CharField( title = forms.CharField(
max_length=100, max_length=100,
required=True, required=True,
widget=forms.TextInput(attrs={'class': 'form-control'}), widget=forms.TextInput(attrs={"class": "form-control"}),
label=_('Summary of the problem'), label=_("Summary of the problem"),
) )
body = forms.CharField( body = forms.CharField(
widget=forms.Textarea(attrs={'class': 'form-control'}), widget=forms.Textarea(attrs={"class": "form-control"}),
label=_('Description of your issue'), label=_("Description of your issue"),
required=True, required=True,
help_text=_( help_text=_("Please be as descriptive as possible and include all details"),
'Please be as descriptive as possible and include all details'),
) )
priority = forms.ChoiceField( priority = forms.ChoiceField(
widget=forms.Select(attrs={'class': 'form-control'}), widget=forms.Select(attrs={"class": "form-control"}),
choices=Ticket.PRIORITY_CHOICES, choices=Ticket.PRIORITY_CHOICES,
required=True, required=True,
initial=getattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY', '3'), initial=getattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY", "3"),
label=_('Priority'), label=_("Priority"),
help_text=_( help_text=_("Please select a priority carefully. If unsure, leave it as '3'."),
"Please select a priority carefully. If unsure, leave it as '3'."),
) )
due_date = forms.DateTimeField( due_date = forms.DateTimeField(
widget=forms.TextInput( widget=forms.TextInput(attrs={"class": "form-control", "autocomplete": "off"}),
attrs={'class': 'form-control', 'autocomplete': 'off'}),
required=False, required=False,
input_formats=[CUSTOMFIELD_DATE_FORMAT, input_formats=[
CUSTOMFIELD_DATETIME_FORMAT, '%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"], CUSTOMFIELD_DATE_FORMAT,
label=_('Due on'), CUSTOMFIELD_DATETIME_FORMAT,
"%d/%m/%Y",
"%m/%d/%Y",
"%d.%m.%Y",
],
label=_("Due on"),
) )
if helpdesk_settings.HELPDESK_ENABLE_ATTACHMENTS: if helpdesk_settings.HELPDESK_ENABLE_ATTACHMENTS:
attachment = forms.FileField( attachment = forms.FileField(
widget=forms.FileInput(attrs={'class': 'form-control-file'}), widget=forms.FileInput(attrs={"class": "form-control-file"}),
required=False, required=False,
label=_('Attach File'), label=_("Attach File"),
help_text=_('You can attach a file to this ticket. ' help_text=_(
'Only file types such as plain text (.txt), ' "You can attach a file to this ticket. "
'a document (.pdf, .docx, or .odt), ' "Only file types such as plain text (.txt), "
'or screenshot (.png or .jpg) may be uploaded.'), "a document (.pdf, .docx, or .odt), "
validators=[validate_file_extension] "or screenshot (.png or .jpg) may be uploaded."
),
validators=[validate_file_extension],
) )
class Media: class Media:
js = ('helpdesk/js/init_due_date.js', js = ("helpdesk/js/init_due_date.js", "helpdesk/js/init_datetime_classes.js")
'helpdesk/js/init_datetime_classes.js')
def __init__(self, kbcategory=None, *args, **kwargs): def __init__(self, kbcategory=None, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
if kbcategory: if kbcategory:
self.fields['kbitem'] = forms.ChoiceField( self.fields["kbitem"] = forms.ChoiceField(
widget=forms.Select(attrs={'class': 'form-control'}), widget=forms.Select(attrs={"class": "form-control"}),
required=False, required=False,
label=_('Knowledge Base Item'), label=_("Knowledge Base Item"),
choices=[(kbi.pk, kbi.title) for kbi in KBItem.objects.filter( choices=[
category=kbcategory.pk, enabled=True)], (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): def _add_form_custom_fields(self, staff_only_filter=None):
@ -316,38 +343,37 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
for field in queryset: for field in queryset:
instanceargs = { instanceargs = {
'label': field.label, "label": field.label,
'help_text': field.help_text, "help_text": field.help_text,
'required': field.required, "required": field.required,
} }
self.customfield_to_field(field, instanceargs) self.customfield_to_field(field, instanceargs)
def _get_queue(self): def _get_queue(self):
# this procedure is re-defined for public submission form # 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): def _create_ticket(self):
queue = self._get_queue() queue = self._get_queue()
kbitem = None kbitem = None
if 'kbitem' in self.cleaned_data: if "kbitem" in self.cleaned_data:
kbitem = KBItem.objects.get(id=int(self.cleaned_data['kbitem'])) kbitem = KBItem.objects.get(id=int(self.cleaned_data["kbitem"]))
ticket = Ticket( ticket = Ticket(
title=self.cleaned_data['title'], title=self.cleaned_data["title"],
submitter_email=self.cleaned_data['submitter_email'], submitter_email=self.cleaned_data["submitter_email"],
created=timezone.now(), created=timezone.now(),
status=Ticket.OPEN_STATUS, status=Ticket.OPEN_STATUS,
queue=queue, queue=queue,
description=self.cleaned_data['body'], description=self.cleaned_data["body"],
priority=self.cleaned_data.get( priority=self.cleaned_data.get(
'priority', "priority", getattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY", "3")
getattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY", "3")
), ),
due_date=self.cleaned_data.get( due_date=self.cleaned_data.get(
'due_date', "due_date", getattr(settings, "HELPDESK_PUBLIC_TICKET_DUE_DATE", None)
getattr(settings, "HELPDESK_PUBLIC_TICKET_DUE_DATE", None) )
) or None, or None,
kbitem=kbitem, kbitem=kbitem,
) )
@ -357,18 +383,19 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
ticket.save_custom_field_values(self.cleaned_data) ticket.save_custom_field_values(self.cleaned_data)
def _create_follow_up(self, ticket, title, user=None): def _create_follow_up(self, ticket, title, user=None):
followup = FollowUp(ticket=ticket, followup = FollowUp(
title=title, ticket=ticket,
date=timezone.now(), title=title,
public=True, date=timezone.now(),
comment=self.cleaned_data['body'], public=True,
) comment=self.cleaned_data["body"],
)
if user: if user:
followup.user = user followup.user = user
return followup return followup
def _attach_files_to_follow_up(self, followup): def _attach_files_to_follow_up(self, followup):
files = self.cleaned_data.get('attachment') files = self.cleaned_data.get("attachment")
if files: if files:
files = process_attachments(followup, [files]) files = process_attachments(followup, [files])
return files return files
@ -376,13 +403,18 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
@staticmethod @staticmethod
def _send_messages(ticket, queue, followup, files, user=None): def _send_messages(ticket, queue, followup, files, user=None):
context = safe_template_context(ticket) context = safe_template_context(ticket)
context['comment'] = followup.comment context["comment"] = followup.comment
roles = {'submitter': ('newticket_submitter', context), roles = {
'new_ticket_cc': ('newticket_cc', context), "submitter": ("newticket_submitter", context),
'ticket_cc': ('newticket_cc', context)} "new_ticket_cc": ("newticket_cc", context),
if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign: "ticket_cc": ("newticket_cc", context),
roles['assigned_to'] = ('assigned_owner', context) }
if (
ticket.assigned_to
and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign
):
roles["assigned_to"] = ("assigned_owner", context)
ticket.send( ticket.send(
roles, roles,
fail_silently=True, fail_silently=True,
@ -394,26 +426,29 @@ class TicketForm(AbstractTicketForm):
""" """
Ticket Form creation for registered users. Ticket Form creation for registered users.
""" """
submitter_email = forms.EmailField( submitter_email = forms.EmailField(
required=False, required=False,
label=_('Submitter E-Mail Address'), label=_("Submitter E-Mail Address"),
widget=forms.TextInput( widget=forms.TextInput(attrs={"class": "form-control", "type": "email"}),
attrs={'class': 'form-control', 'type': 'email'}), help_text=_(
help_text=_('This e-mail address will receive copies of all public ' "This e-mail address will receive copies of all public "
'updates to this ticket.'), "updates to this ticket."
),
) )
assigned_to = forms.ChoiceField( assigned_to = forms.ChoiceField(
widget=( widget=(
forms.Select(attrs={'class': 'form-control'}) forms.Select(attrs={"class": "form-control"})
if not helpdesk_settings.HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO if not helpdesk_settings.HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO
else forms.HiddenInput() else forms.HiddenInput()
), ),
required=False, required=False,
label=_('Case owner'), label=_("Case owner"),
help_text=_('If you select an owner other than yourself, they\'ll be ' help_text=_(
'e-mailed details of this ticket immediately.'), "If you select an owner other than yourself, they'll be "
"e-mailed details of this ticket immediately."
choices=() ),
choices=(),
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -424,15 +459,18 @@ class TicketForm(AbstractTicketForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['queue'].choices = queue_choices self.fields["queue"].choices = queue_choices
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS: if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS:
assignable_users = User.objects.filter( 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: else:
assignable_users = User.objects.filter( assignable_users = User.objects.filter(is_active=True).order_by(
is_active=True).order_by(User.USERNAME_FIELD) User.USERNAME_FIELD
self.fields['assigned_to'].choices = [ )
('', '--------')] + [(u.id, u.get_username()) for u in assignable_users] self.fields["assigned_to"].choices = [("", "--------")] + [
(u.id, u.get_username()) for u in assignable_users
]
self._add_form_custom_fields() self._add_form_custom_fields()
def save(self, user): def save(self, user):
@ -441,9 +479,9 @@ class TicketForm(AbstractTicketForm):
""" """
ticket, queue = self._create_ticket() ticket, queue = self._create_ticket()
if self.cleaned_data['assigned_to']: if self.cleaned_data["assigned_to"]:
try: 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 ticket.assigned_to = u
except User.DoesNotExist: except User.DoesNotExist:
ticket.assigned_to = None ticket.assigned_to = None
@ -451,12 +489,12 @@ class TicketForm(AbstractTicketForm):
self._create_custom_fields(ticket) self._create_custom_fields(ticket)
if self.cleaned_data['assigned_to']: if self.cleaned_data["assigned_to"]:
title = _('Ticket Opened & Assigned to %(name)s') % { title = _("Ticket Opened & Assigned to %(name)s") % {
'name': ticket.get_assigned_to or _("<invalid user>") "name": ticket.get_assigned_to or _("<invalid user>")
} }
else: else:
title = _('Ticket Opened') title = _("Ticket Opened")
followup = self._create_follow_up(ticket, title=title, user=user) followup = self._create_follow_up(ticket, title=title, user=user)
followup.save() followup.save()
@ -468,11 +506,9 @@ class TicketForm(AbstractTicketForm):
# emit signal when the TicketForm.save is done # emit signal when the TicketForm.save is done
new_ticket_done.send(sender="TicketForm", ticket=ticket) new_ticket_done.send(sender="TicketForm", ticket=ticket)
self._send_messages(ticket=ticket, self._send_messages(
queue=queue, ticket=ticket, queue=queue, followup=followup, files=files, user=user
followup=followup, )
files=files,
user=user)
return ticket return ticket
@ -480,12 +516,12 @@ class PublicTicketForm(AbstractTicketForm):
""" """
Ticket Form creation for all users (public-facing). Ticket Form creation for all users (public-facing).
""" """
submitter_email = forms.EmailField( submitter_email = forms.EmailField(
widget=forms.TextInput( widget=forms.TextInput(attrs={"class": "form-control", "type": "email"}),
attrs={'class': 'form-control', 'type': 'email'}),
required=True, required=True,
label=_('Your E-Mail Address'), label=_("Your E-Mail Address"),
help_text=_('We will e-mail you when your ticket is updated.'), help_text=_("We will e-mail you when your ticket is updated."),
) )
def __init__(self, hidden_fields=(), readonly_fields=(), *args, **kwargs): def __init__(self, hidden_fields=(), readonly_fields=(), *args, **kwargs):
@ -502,14 +538,13 @@ class PublicTicketForm(AbstractTicketForm):
self.fields[field].disabled = True self.fields[field].disabled = True
field_deletion_table = { field_deletion_table = {
'queue': 'HELPDESK_PUBLIC_TICKET_QUEUE', "queue": "HELPDESK_PUBLIC_TICKET_QUEUE",
'priority': 'HELPDESK_PUBLIC_TICKET_PRIORITY', "priority": "HELPDESK_PUBLIC_TICKET_PRIORITY",
'due_date': 'HELPDESK_PUBLIC_TICKET_DUE_DATE', "due_date": "HELPDESK_PUBLIC_TICKET_DUE_DATE",
} }
for field_name, field_setting_key in field_deletion_table.items(): for field_name, field_setting_key in field_deletion_table.items():
has_settings_default_value = getattr( has_settings_default_value = getattr(settings, field_setting_key, None)
settings, field_setting_key, None)
if has_settings_default_value is not None: if has_settings_default_value is not None:
del self.fields[field_name] del self.fields[field_name]
@ -520,12 +555,13 @@ class PublicTicketForm(AbstractTicketForm):
"There are no public queues defined - public ticket creation is impossible" "There are no public queues defined - public ticket creation is impossible"
) )
if 'queue' in self.fields: if "queue" in self.fields:
self.fields['queue'].choices = [('', '--------')] + [ self.fields["queue"].choices = [("", "--------")] + [
(q.id, q.title) for q in public_queues] (q.id, q.title) for q in public_queues
]
def _get_queue(self): 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 # force queue to be the pre-defined one
# (only for public submissions) # (only for public submissions)
public_queue = Queue.objects.filter( public_queue = Queue.objects.filter(
@ -534,12 +570,12 @@ class PublicTicketForm(AbstractTicketForm):
if not public_queue: if not public_queue:
logger.fatal( logger.fatal(
"Public queue '%s' is configured as default but can't be found", "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 return public_queue
else: else:
# get the queue user entered # 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): def save(self, user):
""" """
@ -553,7 +589,8 @@ class PublicTicketForm(AbstractTicketForm):
self._create_custom_fields(ticket) self._create_custom_fields(ticket)
followup = self._create_follow_up( followup = self._create_follow_up(
ticket, title=_('Ticket Opened Via Web'), user=user) ticket, title=_("Ticket Opened Via Web"), user=user
)
followup.save() followup.save()
files = self._attach_files_to_follow_up(followup) files = self._attach_files_to_follow_up(followup)
@ -561,161 +598,174 @@ class PublicTicketForm(AbstractTicketForm):
# emit signal when the PublicTicketForm.save is done # emit signal when the PublicTicketForm.save is done
new_ticket_done.send(sender="PublicTicketForm", ticket=ticket) new_ticket_done.send(sender="PublicTicketForm", ticket=ticket)
self._send_messages(ticket=ticket, self._send_messages(ticket=ticket, queue=queue, followup=followup, files=files)
queue=queue,
followup=followup,
files=files)
return ticket return ticket
class UserSettingsForm(forms.ModelForm): class UserSettingsForm(forms.ModelForm):
class Meta: class Meta:
model = UserSettings model = UserSettings
exclude = ['user', 'settings_pickled'] exclude = ["user", "settings_pickled"]
class EmailIgnoreForm(forms.ModelForm): class EmailIgnoreForm(forms.ModelForm):
class Meta: class Meta:
model = IgnoreEmail model = IgnoreEmail
exclude = [] exclude = []
class TicketCCForm(forms.ModelForm): 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: class Meta:
model = TicketCC model = TicketCC
exclude = ('ticket',) exclude = ("ticket",)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(TicketCCForm, self).__init__(*args, **kwargs) super(TicketCCForm, self).__init__(*args, **kwargs)
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC: if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
users = User.objects.filter( users = User.objects.filter(is_active=True, is_staff=True).order_by(
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) User.USERNAME_FIELD
)
else: else:
users = User.objects.filter( users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD)
is_active=True).order_by(User.USERNAME_FIELD) self.fields["user"].queryset = users
self.fields['user'].queryset = users
class TicketCCUserForm(forms.ModelForm): 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): def __init__(self, *args, **kwargs):
super(TicketCCUserForm, self).__init__(*args, **kwargs) super(TicketCCUserForm, self).__init__(*args, **kwargs)
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC: if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
users = User.objects.filter( users = User.objects.filter(is_active=True, is_staff=True).order_by(
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) User.USERNAME_FIELD
)
else: else:
users = User.objects.filter( users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD)
is_active=True).order_by(User.USERNAME_FIELD) self.fields["user"].queryset = users
self.fields['user'].queryset = users
class Meta: class Meta:
model = TicketCC model = TicketCC
exclude = ('ticket', 'email',) exclude = (
"ticket",
"email",
)
class TicketCCEmailForm(forms.ModelForm): 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): def __init__(self, *args, **kwargs):
super(TicketCCEmailForm, self).__init__(*args, **kwargs) super(TicketCCEmailForm, self).__init__(*args, **kwargs)
class Meta: class Meta:
model = TicketCC model = TicketCC
exclude = ('ticket', 'user',) exclude = (
"ticket",
"user",
)
class TicketDependencyForm(forms.ModelForm): 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: class Meta:
model = TicketDependency model = TicketDependency
fields = ('depends_on',) fields = ("depends_on",)
def __init__(self, ticket, *args, **kwargs): 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 # 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): 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: class Meta:
model = TicketDependency model = TicketDependency
fields = ('ticket',) fields = ("ticket",)
def __init__(self, ticket, *args, **kwargs): 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 # 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): class MultipleTicketSelectForm(forms.Form):
tickets = forms.ModelMultipleChoiceField( tickets = forms.ModelMultipleChoiceField(
label=_('Tickets to merge'), label=_("Tickets to merge"),
queryset=Ticket.objects.filter(merged_to=None), 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): def clean_tickets(self):
tickets = self.cleaned_data.get('tickets') tickets = self.cleaned_data.get("tickets")
if len(tickets) < 2: if len(tickets) < 2:
raise ValidationError(_('Please choose at least 2 tickets.')) raise ValidationError(_("Please choose at least 2 tickets."))
if len(tickets) > 4: if len(tickets) > 4:
raise ValidationError( raise ValidationError(_("Impossible to merge more than 4 tickets..."))
_('Impossible to merge more than 4 tickets...')) queues = tickets.order_by("queue").distinct().values_list("queue", flat=True)
queues = tickets.order_by('queue').distinct(
).values_list('queue', flat=True)
if len(queues) != 1: if len(queues) != 1:
raise ValidationError( 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 return tickets
class ChecklistTemplateForm(forms.ModelForm): class ChecklistTemplateForm(forms.ModelForm):
name = forms.CharField( name = forms.CharField(
widget=forms.TextInput(attrs={'class': 'form-control'}), widget=forms.TextInput(attrs={"class": "form-control"}),
required=True, required=True,
) )
task_list = forms.JSONField(widget=forms.HiddenInput()) task_list = forms.JSONField(widget=forms.HiddenInput())
class Meta: class Meta:
model = ChecklistTemplate model = ChecklistTemplate
fields = ('name', 'task_list') fields = ("name", "task_list")
def clean_task_list(self): 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)) return list(map(lambda task: task.strip(), task_list))
class ChecklistForm(forms.ModelForm): class ChecklistForm(forms.ModelForm):
name = forms.CharField( name = forms.CharField(
widget=forms.TextInput(attrs={'class': 'form-control'}), widget=forms.TextInput(attrs={"class": "form-control"}),
required=True, required=True,
) )
class Meta: class Meta:
model = Checklist model = Checklist
fields = ('name',) fields = ("name",)
class CreateChecklistForm(ChecklistForm): class CreateChecklistForm(ChecklistForm):
checklist_template = forms.ModelChoiceField( checklist_template = forms.ModelChoiceField(
label=_("Template"), label=_("Template"),
queryset=ChecklistTemplate.objects.all(), queryset=ChecklistTemplate.objects.all(),
widget=forms.Select(attrs={'class': 'form-control'}), widget=forms.Select(attrs={"class": "form-control"}),
required=False, required=False,
) )
class Meta(ChecklistForm.Meta): class Meta(ChecklistForm.Meta):
fields = ('checklist_template', 'name') fields = ("checklist_template", "name")
class FormControlDeleteFormSet(forms.BaseInlineFormSet): class FormControlDeleteFormSet(forms.BaseInlineFormSet):
deletion_widget = forms.CheckboxInput(attrs={'class': 'form-control'}) deletion_widget = forms.CheckboxInput(attrs={"class": "form-control"})

View File

@ -6,34 +6,52 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
lib.py - Common functions (eg multipart e-mail) lib.py - Common functions (eg multipart e-mail)
""" """
from datetime import date, datetime, time from datetime import date, datetime, time
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError, ImproperlyConfigured from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.utils.encoding import smart_str 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 logging
import mimetypes import mimetypes
logger = logging.getLogger('helpdesk') logger = logging.getLogger("helpdesk")
def ticket_template_context(ticket): def ticket_template_context(ticket):
context = {} context = {}
for field in ('title', 'created', 'modified', 'submitter_email', for field in (
'status', 'get_status_display', 'on_hold', 'description', "title",
'resolution', 'priority', 'get_priority_display', "created",
'last_escalation', 'ticket', 'ticket_for_url', 'merged_to', "modified",
'get_status', 'ticket_url', 'staff_url', '_get_assigned_to' "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) attr = getattr(ticket, field, None)
if callable(attr): if callable(attr):
context[field] = '%s' % attr() context[field] = "%s" % attr()
else: else:
context[field] = attr context[field] = attr
context['assigned_to'] = context['_get_assigned_to'] context["assigned_to"] = context["_get_assigned_to"]
return context return context
@ -41,7 +59,7 @@ def ticket_template_context(ticket):
def queue_template_context(queue): def queue_template_context(queue):
context = {} 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) attr = getattr(queue, field, None)
if callable(attr): if callable(attr):
context[field] = attr() context[field] = attr()
@ -67,10 +85,10 @@ def safe_template_context(ticket):
""" """
context = { context = {
'queue': queue_template_context(ticket.queue), "queue": queue_template_context(ticket.queue),
'ticket': ticket_template_context(ticket), "ticket": ticket_template_context(ticket),
} }
context['ticket']['queue'] = context['queue'] context["ticket"]["queue"] = context["queue"]
return context return context
@ -87,41 +105,42 @@ def text_is_spam(text, request):
return False return False
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
try: try:
site = Site.objects.get_current() site = Site.objects.get_current()
except ImproperlyConfigured: except ImproperlyConfigured:
site = Site(domain='configure-django-sites.com') site = Site(domain="configure-django-sites.com")
# see https://akismet.readthedocs.io/en/latest/overview.html#using-akismet # see https://akismet.readthedocs.io/en/latest/overview.html#using-akismet
apikey = None apikey = None
if hasattr(settings, 'TYPEPAD_ANTISPAM_API_KEY'): if hasattr(settings, "TYPEPAD_ANTISPAM_API_KEY"):
apikey = 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 # new env var expected by python-akismet package
apikey = settings.PYTHON_AKISMET_API_KEY apikey = settings.PYTHON_AKISMET_API_KEY
elif hasattr(settings, 'AKISMET_API_KEY'): elif hasattr(settings, "AKISMET_API_KEY"):
# deprecated, but kept for backward compatibility # deprecated, but kept for backward compatibility
apikey = settings.AKISMET_API_KEY apikey = settings.AKISMET_API_KEY
else: else:
return False return False
ak = Akismet( ak = Akismet(
blog_url='http://%s/' % site.domain, blog_url="http://%s/" % site.domain,
key=apikey, key=apikey,
) )
if hasattr(settings, 'TYPEPAD_ANTISPAM_API_KEY'): if hasattr(settings, "TYPEPAD_ANTISPAM_API_KEY"):
ak.baseurl = 'api.antispam.typepad.com/1.1/' ak.baseurl = "api.antispam.typepad.com/1.1/"
if ak.verify_key(): if ak.verify_key():
ak_data = { ak_data = {
'user_ip': request.META.get('REMOTE_ADDR', '127.0.0.1'), "user_ip": request.META.get("REMOTE_ADDR", "127.0.0.1"),
'user_agent': request.headers.get('User-Agent', ''), "user_agent": request.headers.get("User-Agent", ""),
'referrer': request.headers.get('Referer', ''), "referrer": request.headers.get("Referer", ""),
'comment_type': 'comment', "comment_type": "comment",
'comment_author': '', "comment_author": "",
} }
return ak.comment_check(smart_str(text), data=ak_data) 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): def process_attachments(followup, attached_files):
max_email_attachment_size = getattr( max_email_attachment_size = getattr(
settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000) settings, "HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE", 512000
)
attachments = [] attachments = []
errors = set() errors = set()
for attached in attached_files: for attached in attached_files:
if attached.size: if attached.size:
from helpdesk.models import FollowUpAttachment from helpdesk.models import FollowUpAttachment
@ -145,9 +164,9 @@ def process_attachments(followup, attached_files):
followup=followup, followup=followup,
file=attached, file=attached,
filename=filename, filename=filename,
mime_type=attached.content_type or mime_type=attached.content_type
mimetypes.guess_type(filename, strict=False)[0] or or mimetypes.guess_type(filename, strict=False)[0]
'application/octet-stream', or "application/octet-stream",
size=attached.size, size=attached.size,
) )
try: try:
@ -176,7 +195,7 @@ def format_time_spent(time_spent):
if time_spent: if time_spent:
time_spent = "{0:02d}h:{1:02d}m".format( time_spent = "{0:02d}h:{1:02d}m".format(
int(time_spent.total_seconds()) // 3600, int(time_spent.total_seconds()) // 3600,
int(time_spent.total_seconds()) % 3600 // 60 int(time_spent.total_seconds()) % 3600 // 60,
) )
else: else:
time_spent = "" time_spent = ""
@ -184,7 +203,7 @@ def format_time_spent(time_spent):
def convert_value(value): 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: if type(value) is datetime:
return value.strftime(CUSTOMFIELD_DATETIME_FORMAT) return value.strftime(CUSTOMFIELD_DATETIME_FORMAT)
elif type(value) is date: elif type(value) is date:
@ -201,39 +220,65 @@ def daily_time_spent_calculation(earliest, latest, open_hours):
time_spent_seconds = 0 time_spent_seconds = 0
# avoid rendering day in different locale # avoid rendering day in different locale
weekday = ('monday', 'tuesday', 'wednesday', 'thursday', weekday = (
'friday', 'saturday', 'sunday')[earliest.weekday()] "monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
)[earliest.weekday()]
# enforce correct settings # enforce correct settings
MIDNIGHT = 23.9999 MIDNIGHT = 23.9999
start, end = open_hours.get(weekday, (0, MIDNIGHT)) start, end = open_hours.get(weekday, (0, MIDNIGHT))
if not 0 <= start <= end <= MIDNIGHT: if not 0 <= start <= end <= MIDNIGHT:
raise ImproperlyConfigured("HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS" raise ImproperlyConfigured(
f" setting for {weekday} out of (0, 23.9999) boundary") "HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS"
f" setting for {weekday} out of (0, 23.9999) boundary"
)
# transform decimals to minutes and seconds # transform decimals to minutes and seconds
start_hour, start_minute, start_second = int(start), int(start % 1 * 60), int(start * 60 % 1 * 60) start_hour, start_minute, start_second = (
end_hour, end_minute, end_second = int(end), int(end % 1 * 60), int(end * 60 % 1 * 60) 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 # translate time for delta calculation
earliest_f = earliest.hour + earliest.minute / 60 + earliest.second / 3600 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 time is midnight and close hour is midnight, add a second to the time spent
if latest_f >= MIDNIGHT and end == MIDNIGHT: if latest_f >= MIDNIGHT and end == MIDNIGHT:
time_spent_seconds += 1 time_spent_seconds += 1
if earliest_f < start: 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: elif earliest_f >= end:
earliest = earliest.replace(hour=end_hour, minute=end_minute, second=end_second) earliest = earliest.replace(hour=end_hour, minute=end_minute, second=end_second)
if latest_f < start: 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: elif latest_f >= end:
latest = latest.replace(hour=end_hour, minute=end_minute, second=end_second) latest = latest.replace(hour=end_hour, minute=end_minute, second=end_second)
day_delta = latest - earliest day_delta = latest - earliest
time_spent_seconds += day_delta.seconds time_spent_seconds += day_delta.seconds
return time_spent_seconds return time_spent_seconds

View File

@ -14,56 +14,56 @@ from django.core.management.base import BaseCommand, CommandError
from helpdesk.models import EscalationExclusion, Queue from helpdesk.models import EscalationExclusion, Queue
day_names = { day_names = {
'monday': 0, "monday": 0,
'tuesday': 1, "tuesday": 1,
'wednesday': 2, "wednesday": 2,
'thursday': 3, "thursday": 3,
'friday': 4, "friday": 4,
'saturday': 5, "saturday": 5,
'sunday': 6, "sunday": 6,
} }
class Command(BaseCommand): class Command(BaseCommand):
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'-d', "-d",
'--days', "--days",
nargs='*', nargs="*",
choices=list(day_names.keys()), choices=list(day_names.keys()),
required=True, 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( parser.add_argument(
'-o', "-o",
'--occurrences', "--occurrences",
default=1, default=1,
type=int, 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( parser.add_argument(
'-q', "-q",
'--queues', "--queues",
nargs='*', nargs="*",
choices=list(Queue.objects.values_list('slug', flat=True)), choices=list(Queue.objects.values_list("slug", flat=True)),
help='Queues to include (default: all). Enter the queues slug as space separated list.' help="Queues to include (default: all). Enter the queues slug as space separated list.",
) )
parser.add_argument( parser.add_argument(
'-x', "-x",
'--exclude-verbosely', "--exclude-verbosely",
action='store_true', action="store_true",
default=False, default=False,
help='Display a list of dates excluded' help="Display a list of dates excluded",
) )
def handle(self, *args, **options): def handle(self, *args, **options):
days = options['days'] days = options["days"]
occurrences = options['occurrences'] occurrences = options["occurrences"]
verbose = options['exclude_verbosely'] verbose = options["exclude_verbosely"]
queue_slugs = options['queues'] queue_slugs = options["queues"]
if not (days and occurrences): if not (days and occurrences):
raise CommandError('One or more occurrences must be specified.') raise CommandError("One or more occurrences must be specified.")
queues = [] queues = []
if queue_slugs is not None: if queue_slugs is not None:
@ -77,12 +77,13 @@ class Command(BaseCommand):
if day == workdate.weekday(): if day == workdate.weekday():
if EscalationExclusion.objects.filter(date=workdate).count() == 0: if EscalationExclusion.objects.filter(date=workdate).count() == 0:
esc = EscalationExclusion.objects.create( esc = EscalationExclusion.objects.create(
name=f'Auto Exclusion for {day_name}', name=f"Auto Exclusion for {day_name}", date=workdate
date=workdate
) )
if verbose: 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: for q in queues:
esc.queues.add(q) esc.queues.add(q)

View File

@ -22,25 +22,24 @@ from helpdesk.models import Queue
class Command(BaseCommand): class Command(BaseCommand):
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'-q', "-q",
'--queues', "--queues",
nargs='*', nargs="*",
choices=list(Queue.objects.values_list('slug', flat=True)), choices=list(Queue.objects.values_list("slug", flat=True)),
help='Queues to include (default: all). Enter the queues slug as space separated list.' help="Queues to include (default: all). Enter the queues slug as space separated list.",
) )
parser.add_argument( parser.add_argument(
'-x', "-x",
'--escalate-verbosely', "--escalate-verbosely",
action='store_true', action="store_true",
default=False, default=False,
help='Display a list of dates excluded' help="Display a list of dates excluded",
) )
def handle(self, *args, **options): def handle(self, *args, **options):
queue_slugs = options['queues'] queue_slugs = options["queues"]
if queue_slugs is not None: if queue_slugs is not None:
queues = Queue.objects.filter(slug__in=queue_slugs) queues = Queue.objects.filter(slug__in=queue_slugs)
@ -53,16 +52,17 @@ class Command(BaseCommand):
if q.permission_name: if q.permission_name:
self.stdout.write( self.stdout.write(
f" .. already has `permission_name={q.permission_name}`") f" .. already has `permission_name={q.permission_name}`"
)
basename = q.permission_name[9:] basename = q.permission_name[9:]
else: else:
basename = q.generate_permission_name() basename = q.generate_permission_name()
self.stdout.write( self.stdout.write(
f" .. generated `permission_name={q.permission_name}`") f" .. generated `permission_name={q.permission_name}`"
)
q.save() q.save()
self.stdout.write( self.stdout.write(f" .. checking permission codename `{basename}`")
f" .. checking permission codename `{basename}`")
try: try:
Permission.objects.create( Permission.objects.create(

View File

@ -20,10 +20,12 @@ User = get_user_model()
class Command(BaseCommand): class Command(BaseCommand):
"""create_usersettings command""" """create_usersettings command"""
help = _('Check for user without django-helpdesk UserSettings ' help = _(
'and create settings if required. Uses ' "Check for user without django-helpdesk UserSettings "
'settings.DEFAULT_USER_SETTINGS which can be overridden to ' "and create settings if required. Uses "
'suit your situation.') "settings.DEFAULT_USER_SETTINGS which can be overridden to "
"suit your situation."
)
def handle(self, *args, **options): def handle(self, *args, **options):
"""handle command line""" """handle command line"""

View File

@ -20,34 +20,36 @@ from helpdesk.models import EscalationExclusion, Queue, Ticket
class Command(BaseCommand): class Command(BaseCommand):
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'-q', "-q",
'--queues', "--queues",
nargs='*', nargs="*",
choices=list(Queue.objects.values_list('slug', flat=True)), choices=list(Queue.objects.values_list("slug", flat=True)),
help='Queues to include (default: all). Enter the queues slug as space separated list.' help="Queues to include (default: all). Enter the queues slug as space separated list.",
) )
parser.add_argument( parser.add_argument(
'-x', "-x",
'--escalate-verbosely', "--escalate-verbosely",
action='store_true', action="store_true",
default=False, default=False,
help='Display escalated tickets' help="Display escalated tickets",
) )
parser.add_argument( parser.add_argument(
'-n', "-n",
'--notify-only', "--notify-only",
action='store_true', action="store_true",
default=False, default=False,
help='Send email reminder but dont escalate tickets' help="Send email reminder but dont escalate tickets",
) )
def handle(self, *args, **options): def handle(self, *args, **options):
verbose = options['escalate_verbosely'] verbose = options["escalate_verbosely"]
notify_only = options['notify_only'] notify_only = options["notify_only"]
queue_slugs = options['queues'] queue_slugs = options["queues"]
# Only include queues with escalation configured # 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: if queue_slugs is not None:
queues = queues.filter(slug__in=queue_slugs) queues = queues.filter(slug__in=queue_slugs)
@ -68,17 +70,15 @@ class Command(BaseCommand):
req_last_escl_date = timezone.now() - timedelta(days=days) req_last_escl_date = timezone.now() - timedelta(days=days)
for ticket in queue.ticket_set.filter( for ticket in (
status__in=Ticket.OPEN_STATUSES queue.ticket_set.filter(status__in=Ticket.OPEN_STATUSES)
).exclude( .exclude(priority=1)
priority=1 .filter(Q(on_hold__isnull=True) | Q(on_hold=False))
).filter( .filter(
Q(on_hold__isnull=True) | Q(on_hold=False) Q(last_escalation__lte=req_last_escl_date)
).filter( | Q(last_escalation__isnull=True, created__lte=req_last_escl_date)
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.last_escalation = timezone.now()
ticket.priority -= 1 ticket.priority -= 1
ticket.save() ticket.save()
@ -86,24 +86,29 @@ class Command(BaseCommand):
context = safe_template_context(ticket) context = safe_template_context(ticket)
ticket.send( ticket.send(
{'submitter': ('escalated_submitter', context), {
'ticket_cc': ('escalated_cc', context), "submitter": ("escalated_submitter", context),
'assigned_to': ('escalated_owner', context)}, "ticket_cc": ("escalated_cc", context),
"assigned_to": ("escalated_owner", context),
},
fail_silently=True, fail_silently=True,
) )
if verbose: 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: if not notify_only:
followup = ticket.followup_set.create( followup = ticket.followup_set.create(
title=_('Ticket Escalated'), title=_("Ticket Escalated"),
public=True, 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( followup.ticketchange_set.create(
field=_('Priority'), field=_("Priority"),
old_value=ticket.priority + 1, old_value=ticket.priority + 1,
new_value=ticket.priority, new_value=ticket.priority,
) )

View File

@ -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 helpdesk, creating tickets from the new messages (or
adding to existing tickets if needed) adding to existing tickets if needed)
""" """
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from helpdesk.email import process_email from helpdesk.email import process_email
class Command(BaseCommand): class Command(BaseCommand):
help = (
help = 'Process django-helpdesk queues and process e-mails via POP3/IMAP or ' \ "Process django-helpdesk queues and process e-mails via POP3/IMAP or "
'from a local mailbox directory as required, feeding them into the helpdesk.' "from a local mailbox directory as required, feeding them into the helpdesk."
)
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'--quiet', "--quiet",
action='store_true', action="store_true",
dest='quiet', dest="quiet",
default=False, 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( parser.add_argument(
'--debug_to_stdout', "--debug_to_stdout",
action='store_true', action="store_true",
dest='debug_to_stdout', dest="debug_to_stdout",
default=False, default=False,
help='Log additional messaging to stdout.', help="Log additional messaging to stdout.",
) )
def handle(self, *args, **options): def handle(self, *args, **options):
quiet = options.get('quiet') quiet = options.get("quiet")
debug_to_stdout = options.get('debug_to_stdout') debug_to_stdout = options.get("debug_to_stdout")
process_email(quiet=quiet, debug_to_stdout=debug_to_stdout) process_email(quiet=quiet, debug_to_stdout=debug_to_stdout)
if __name__ == '__main__': if __name__ == "__main__":
process_email() process_email()

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ def pickle_settings(data):
except ImportError: except ImportError:
import cPickle as pickle import cPickle as pickle
from helpdesk.query import b64encode from helpdesk.query import b64encode
return b64encode(pickle.dumps(data)) return b64encode(pickle.dumps(data))
@ -38,14 +39,12 @@ def populate_usersettings(apps, schema_editor):
noop = lambda *args, **kwargs: None noop = lambda *args, **kwargs: None
class Migration(migrations.Migration):
class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0001_initial'), ("helpdesk", "0001_initial"),
] ]
operations = [ operations = [
migrations.RunPython(populate_usersettings, reverse_code=noop), migrations.RunPython(populate_usersettings, reverse_code=noop),
] ]

View File

@ -4,14 +4,15 @@ import os
from django.db import migrations from django.db import migrations
from django.core import serializers from django.core import serializers
fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures')) fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../fixtures"))
fixture_filename = 'emailtemplate.json' fixture_filename = "emailtemplate.json"
def deserialize_fixture(): def deserialize_fixture():
fixture_file = os.path.join(fixture_dir, fixture_filename) fixture_file = os.path.join(fixture_dir, fixture_filename)
with open(fixture_file, 'rb') as fixture: with open(fixture_file, "rb") as fixture:
return list(serializers.deserialize('json', fixture, ignorenonexistent=True)) return list(serializers.deserialize("json", fixture, ignorenonexistent=True))
def load_fixture(apps, schema_editor): def load_fixture(apps, schema_editor):
@ -27,13 +28,12 @@ def unload_fixture(apps, schema_editor):
objects = deserialize_fixture() objects = deserialize_fixture()
EmailTemplate = apps.get_model("helpdesk", "emailtemplate") 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): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0002_populate_usersettings'), ("helpdesk", "0002_populate_usersettings"),
] ]
operations = [ operations = [

View File

@ -4,23 +4,42 @@ from django.conf import settings
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('helpdesk', '0003_initial_data_import'), ("helpdesk", "0003_initial_data_import"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='QueueMembership', name="QueueMembership",
fields=[ 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')), "id",
('user', models.OneToOneField(verbose_name='User', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), 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={ options={
'verbose_name': 'Queue Membership', "verbose_name": "Queue Membership",
'verbose_name_plural': 'Queue Memberships', "verbose_name_plural": "Queue Memberships",
}, },
bases=(models.Model,), bases=(models.Model,),
), ),

View File

@ -3,25 +3,36 @@ from django.db import models, migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0004_add_per_queue_staff_membership'), ("helpdesk", "0004_add_per_queue_staff_membership"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='escalationexclusion', model_name="escalationexclusion",
name='queues', 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), 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( migrations.AlterField(
model_name='ignoreemail', model_name="ignoreemail",
name='queues', 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), 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( migrations.AlterField(
model_name='presetreply', model_name="presetreply",
name='queues', 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), 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,
),
), ),
] ]

View File

@ -3,25 +3,42 @@ from django.db import models, migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0005_queues_no_null'), ("helpdesk", "0005_queues_no_null"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='queue', model_name="queue",
name='email_address', 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), 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( migrations.AlterField(
model_name='ticket', model_name="ticket",
name='submitter_email', 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), 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( migrations.AlterField(
model_name='ticketcc', model_name="ticketcc",
name='email', 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), 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,
),
), ),
] ]

View File

@ -3,15 +3,18 @@ from django.db import models, migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0006_email_maxlength'), ("helpdesk", "0006_email_maxlength"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='customfield', model_name="customfield",
name='label', name="label",
field=models.CharField(help_text='The display label for this field', max_length=30, verbose_name='Label'), field=models.CharField(
help_text="The display label for this field",
max_length=30,
verbose_name="Label",
),
), ),
] ]

View File

@ -3,15 +3,20 @@ from django.db import models, migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0007_max_length_by_integer'), ("helpdesk", "0007_max_length_by_integer"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='queue', model_name="queue",
name='permission_name', 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), 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,
),
), ),
] ]

View File

@ -7,14 +7,14 @@ from django.utils.translation import gettext_lazy as _
def create_and_assign_permissions(apps, schema_editor): def create_and_assign_permissions(apps, schema_editor):
db_alias = schema_editor.connection.alias db_alias = schema_editor.connection.alias
Permission = apps.get_model('auth', 'Permission') Permission = apps.get_model("auth", "Permission")
ContentType = apps.get_model('contenttypes', 'ContentType') ContentType = apps.get_model("contenttypes", "ContentType")
# Two steps: # Two steps:
# 1. Create the permission for existing Queues # 1. Create the permission for existing Queues
# 2. Assign the permission to user according to QueueMembership objects # 2. Assign the permission to user according to QueueMembership objects
# First step: prepare the permission for each queue # 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(): for q in Queue.objects.using(db_alias).all():
if not q.permission_name: if not q.permission_name:
basename = "queue_access_%s" % q.slug basename = "queue_access_%s" % q.slug
@ -35,7 +35,7 @@ def create_and_assign_permissions(apps, schema_editor):
q.save() q.save()
# Second step: map the permissions according to QueueMembership # 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(): for qm in QueueMembership.objects.using(db_alias).all():
user = qm.user user = qm.user
for q in qm.queues.all(): 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): def revert_queue_membership(apps, schema_editor):
db_alias = schema_editor.connection.alias db_alias = schema_editor.connection.alias
Permission = apps.get_model('auth', 'Permission') Permission = apps.get_model("auth", "Permission")
Queue = apps.get_model('helpdesk', 'Queue') Queue = apps.get_model("helpdesk", "Queue")
QueueMembership = apps.get_model('helpdesk', 'QueueMembership') QueueMembership = apps.get_model("helpdesk", "QueueMembership")
for p in Permission.objects.using(db_alias).all(): for p in Permission.objects.using(db_alias).all():
if p.codename.startswith("queue_access_"): if p.codename.startswith("queue_access_"):
slug = p.codename[13:] slug = p.codename[13:]
@ -66,12 +66,10 @@ def revert_queue_membership(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0008_extra_for_permissions'), ("helpdesk", "0008_extra_for_permissions"),
] ]
operations = [ operations = [
migrations.RunPython(create_and_assign_permissions, migrations.RunPython(create_and_assign_permissions, revert_queue_membership)
revert_queue_membership)
] ]

View File

@ -3,21 +3,20 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0009_migrate_queuemembership'), ("helpdesk", "0009_migrate_queuemembership"),
] ]
operations = [ operations = [
migrations.RemoveField( migrations.RemoveField(
model_name='queuemembership', model_name="queuemembership",
name='queues', name="queues",
), ),
migrations.RemoveField( migrations.RemoveField(
model_name='queuemembership', model_name="queuemembership",
name='user', name="user",
), ),
migrations.DeleteModel( migrations.DeleteModel(
name='QueueMembership', name="QueueMembership",
), ),
] ]

View File

@ -3,20 +3,30 @@ from django.db import models, migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0010_remove_queuemembership'), ("helpdesk", "0010_remove_queuemembership"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='queue', model_name="queue",
name='permission_name', 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'), 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( migrations.AlterField(
model_name='queue', model_name="queue",
name='slug', 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'), 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",
),
), ),
] ]

View File

@ -6,16 +6,22 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('helpdesk', '0011_admin_related_improvements'), ("helpdesk", "0011_admin_related_improvements"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='queue', model_name="queue",
name='default_owner', 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'), 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",
),
), ),
] ]

View File

@ -4,30 +4,66 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0012_queue_default_owner'), ("helpdesk", "0012_queue_default_owner"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='queue', model_name="queue",
name='email_box_local_dir', 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'), 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( migrations.AddField(
model_name='queue', model_name="queue",
name='logging_dir', 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'), 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( migrations.AddField(
model_name='queue', model_name="queue",
name='logging_type', 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'), 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( migrations.AlterField(
model_name='queue', model_name="queue",
name='email_box_type', 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'), 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",
),
), ),
] ]

View File

@ -4,17 +4,18 @@ from django.conf import settings
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0013_email_box_local_dir_and_logging'), ("helpdesk", "0013_email_box_local_dir_and_logging"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='usersettings', model_name="usersettings",
name='user', name="user",
field=models.OneToOneField(to=settings.AUTH_USER_MODEL, field=models.OneToOneField(
related_name='usersettings_helpdesk', to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE), related_name="usersettings_helpdesk",
on_delete=models.CASCADE,
),
), ),
] ]

View File

@ -4,15 +4,21 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0014_usersettings_related_name'), ("helpdesk", "0014_usersettings_related_name"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='queue', model_name="queue",
name='permission_name', 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'), 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",
),
), ),
] ]

View File

@ -4,38 +4,61 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0015_expand_permission_name_size'), ("helpdesk", "0015_expand_permission_name_size"),
] ]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='attachment', name="attachment",
options={'ordering': ('filename',), 'verbose_name': 'Attachment', 'verbose_name_plural': 'Attachments'}, options={
"ordering": ("filename",),
"verbose_name": "Attachment",
"verbose_name_plural": "Attachments",
},
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='emailtemplate', name="emailtemplate",
options={'ordering': ('template_name', 'locale'), 'verbose_name': 'e-mail template', 'verbose_name_plural': 'e-mail templates'}, options={
"ordering": ("template_name", "locale"),
"verbose_name": "e-mail template",
"verbose_name_plural": "e-mail templates",
},
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='followup', name="followup",
options={'ordering': ('date',), 'verbose_name': 'Follow-up', 'verbose_name_plural': 'Follow-ups'}, options={
"ordering": ("date",),
"verbose_name": "Follow-up",
"verbose_name_plural": "Follow-ups",
},
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='kbcategory', name="kbcategory",
options={'ordering': ('title',), 'verbose_name': 'Knowledge base category', 'verbose_name_plural': 'Knowledge base categories'}, options={
"ordering": ("title",),
"verbose_name": "Knowledge base category",
"verbose_name_plural": "Knowledge base categories",
},
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='kbitem', name="kbitem",
options={'ordering': ('title',), 'verbose_name': 'Knowledge base item', 'verbose_name_plural': 'Knowledge base items'}, options={
"ordering": ("title",),
"verbose_name": "Knowledge base item",
"verbose_name_plural": "Knowledge base items",
},
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='presetreply', name="presetreply",
options={'ordering': ('name',), 'verbose_name': 'Pre-set reply', 'verbose_name_plural': 'Pre-set replies'}, options={
"ordering": ("name",),
"verbose_name": "Pre-set reply",
"verbose_name_plural": "Pre-set replies",
},
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='ticketcustomfieldvalue', name="ticketcustomfieldvalue",
unique_together=set([('ticket', 'field')]), unique_together=set([("ticket", "field")]),
), ),
] ]

View File

@ -6,15 +6,21 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0016_alter_model_options'), ("helpdesk", "0016_alter_model_options"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='queue', model_name="queue",
name='default_owner', 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'), 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",
),
), ),
] ]

View File

@ -4,55 +4,99 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0017_default_owner_on_delete_null'), ("helpdesk", "0017_default_owner_on_delete_null"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='followup', model_name="followup",
name='public', 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'), 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( migrations.AlterField(
model_name='ignoreemail', model_name="ignoreemail",
name='keep_in_mailbox', 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?'), 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( migrations.AlterField(
model_name='queue', model_name="queue",
name='allow_email_submission', 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?'), 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( migrations.AlterField(
model_name='queue', model_name="queue",
name='allow_public_submission', 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?'), 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( migrations.AlterField(
model_name='queue', model_name="queue",
name='email_box_ssl', 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?'), 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( migrations.AlterField(
model_name='savedsearch', model_name="savedsearch",
name='shared', name="shared",
field=models.BooleanField(blank=True, default=False, help_text='Should other users see this query?', verbose_name='Shared With Other Users?'), field=models.BooleanField(
blank=True,
default=False,
help_text="Should other users see this query?",
verbose_name="Shared With Other Users?",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='ticket', model_name="ticket",
name='on_hold', 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'), 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( migrations.AlterField(
model_name='ticketcc', model_name="ticketcc",
name='can_update', 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?'), field=models.BooleanField(
blank=True,
default=False,
help_text="Can this CC login and update the ticket?",
verbose_name="Can Update Ticket?",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='ticketcc', model_name="ticketcc",
name='can_view', 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?'), field=models.BooleanField(
blank=True,
default=False,
help_text="Can this CC login to view the ticket details?",
verbose_name="Can View Ticket?",
),
), ),
] ]

View File

@ -8,22 +8,24 @@ def clear_secret_keys(apps, schema_editor):
db_alias = schema_editor.connection.alias db_alias = schema_editor.connection.alias
for ticket in Ticket.objects.using(db_alias).all(): for ticket in Ticket.objects.using(db_alias).all():
ticket.secret_key = '' ticket.secret_key = ""
ticket.save() ticket.save()
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0018_ticket_secret_key'), ("helpdesk", "0018_ticket_secret_key"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='ticket', model_name="ticket",
name='secret_key', name="secret_key",
field=models.CharField(default=helpdesk.models.mk_secret, max_length=36, field=models.CharField(
verbose_name='Secret key needed for viewing/editing ticket by non-logged in users'), 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), migrations.RunPython(clear_secret_keys),
] ]

View File

@ -16,7 +16,7 @@ def unpickle_settings(settings_pickled):
# Python 3 support # Python 3 support
from base64 import decodebytes as b64decode from base64 import decodebytes as b64decode
try: try:
return pickle.loads(b64decode(settings_pickled.encode('utf-8'))) return pickle.loads(b64decode(settings_pickled.encode("utf-8")))
except Exception: except Exception:
return {} return {}
@ -33,41 +33,66 @@ def move_old_values(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0019_ticket_secret_key'), ("helpdesk", "0019_ticket_secret_key"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='usersettings', model_name="usersettings",
name='email_on_ticket_assign', 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?'), 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( migrations.AddField(
model_name='usersettings', model_name="usersettings",
name='email_on_ticket_change', 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?'), 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( migrations.AddField(
model_name='usersettings', model_name="usersettings",
name='login_view_ticketlist', 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?'), 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( migrations.AddField(
model_name='usersettings', model_name="usersettings",
name='tickets_per_page', 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'), 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( migrations.AddField(
model_name='usersettings', model_name="usersettings",
name='use_email_as_submitter', 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?'), 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( migrations.AlterField(
model_name='usersettings', model_name="usersettings",
name='settings_pickled', 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!'), 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), migrations.RunPython(move_old_values),
] ]

View File

@ -4,61 +4,105 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('helpdesk', '0020_depickle_user_settings'), ("helpdesk", "0020_depickle_user_settings"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='kbitem', model_name="kbitem",
name='voted_by', name="voted_by",
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL), field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
), ),
migrations.AlterField( migrations.AlterField(
model_name='followup', model_name="followup",
name='public', 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'), 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( migrations.AlterField(
model_name='ignoreemail', model_name="ignoreemail",
name='keep_in_mailbox', 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?'), 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( migrations.AlterField(
model_name='queue', model_name="queue",
name='allow_email_submission', 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?'), 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( migrations.AlterField(
model_name='queue', model_name="queue",
name='allow_public_submission', 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?'), 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( migrations.AlterField(
model_name='queue', model_name="queue",
name='email_box_ssl', 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?'), 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( migrations.AlterField(
model_name='savedsearch', model_name="savedsearch",
name='shared', name="shared",
field=models.BooleanField(blank=True, default=False, help_text='Should other users see this query?', verbose_name='Shared With Other Users?'), field=models.BooleanField(
blank=True,
default=False,
help_text="Should other users see this query?",
verbose_name="Shared With Other Users?",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='ticket', model_name="ticket",
name='on_hold', 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'), 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( migrations.AlterField(
model_name='ticketcc', model_name="ticketcc",
name='can_update', 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?'), field=models.BooleanField(
blank=True,
default=False,
help_text="Can this CC login and update the ticket?",
verbose_name="Can Update Ticket?",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='ticketcc', model_name="ticketcc",
name='can_view', 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?'), field=models.BooleanField(
blank=True,
default=False,
help_text="Can this CC login to view the ticket details?",
verbose_name="Can View Ticket?",
),
), ),
] ]

View File

@ -4,15 +4,21 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0021_voting_tracker'), ("helpdesk", "0021_voting_tracker"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='followup', model_name="followup",
name='message_id', 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'), 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",
),
), ),
] ]

View File

@ -4,15 +4,18 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0022_add_submitter_email_id_field_to_ticket'), ("helpdesk", "0022_add_submitter_email_id_field_to_ticket"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='queue', model_name="queue",
name='enable_notifications_on_email_events', 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'), 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",
),
), ),
] ]

View File

@ -4,15 +4,16 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0023_add_enable_notifications_on_email_events_to_ticket'), ("helpdesk", "0023_add_enable_notifications_on_email_events_to_ticket"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='followup', model_name="followup",
name='time_spent', name="time_spent",
field=models.DurationField(blank=True, help_text='Time spent on this follow up', null=True), field=models.DurationField(
blank=True, help_text="Time spent on this follow up", null=True
),
), ),
] ]

View File

@ -4,15 +4,18 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0024_time_spent'), ("helpdesk", "0024_time_spent"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='queue', model_name="queue",
name='dedicated_time', name="dedicated_time",
field=models.DurationField(blank=True, help_text='Time to be spent on this Queue in total', null=True), field=models.DurationField(
blank=True,
help_text="Time to be spent on this Queue in total",
null=True,
),
), ),
] ]

View File

@ -6,31 +6,63 @@ import helpdesk.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0025_queue_dedicated_time'), ("helpdesk", "0025_queue_dedicated_time"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='KBIAttachment', name="KBIAttachment",
fields=[ 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')), "id",
('filename', models.CharField(max_length=1000, verbose_name='Filename')), models.AutoField(
('mime_type', models.CharField(max_length=255, verbose_name='MIME Type')), auto_created=True,
('size', models.IntegerField(help_text='Size of this file in bytes', verbose_name='Size')), primary_key=True,
('kbitem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='helpdesk.KBItem', verbose_name='Knowledge base item')), 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={ options={
'verbose_name': 'Attachment', "verbose_name": "Attachment",
'verbose_name_plural': 'Attachments', "verbose_name_plural": "Attachments",
'ordering': ('filename',), "ordering": ("filename",),
'abstract': False, "abstract": False,
}, },
), ),
migrations.RenameModel( migrations.RenameModel(
old_name='Attachment', old_name="Attachment",
new_name='FollowUpAttachment', new_name="FollowUpAttachment",
), ),
] ]

View File

@ -6,66 +6,98 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('helpdesk', '0026_kbitem_attachments'), ("helpdesk", "0026_kbitem_attachments"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='kbcategory', model_name="kbcategory",
name='queue', 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.'), 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( migrations.AddField(
model_name='kbitem', model_name="kbitem",
name='downvoted_by', name="downvoted_by",
field=models.ManyToManyField(related_name='downvotes', to=settings.AUTH_USER_MODEL), field=models.ManyToManyField(
related_name="downvotes", to=settings.AUTH_USER_MODEL
),
), ),
migrations.AddField( migrations.AddField(
model_name='ticket', model_name="ticket",
name='kbitem', 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.'), 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( migrations.AlterField(
model_name='followupattachment', model_name="followupattachment",
name='filename', name="filename",
field=models.CharField(blank=True, max_length=1000, verbose_name='Filename'), field=models.CharField(
blank=True, max_length=1000, verbose_name="Filename"
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='followupattachment', model_name="followupattachment",
name='mime_type', name="mime_type",
field=models.CharField(blank=True, max_length=255, verbose_name='MIME Type'), field=models.CharField(
blank=True, max_length=255, verbose_name="MIME Type"
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='followupattachment', model_name="followupattachment",
name='size', name="size",
field=models.IntegerField(blank=True, help_text='Size of this file in bytes', verbose_name='Size'), field=models.IntegerField(
blank=True, help_text="Size of this file in bytes", verbose_name="Size"
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='kbiattachment', model_name="kbiattachment",
name='filename', name="filename",
field=models.CharField(blank=True, max_length=1000, verbose_name='Filename'), field=models.CharField(
blank=True, max_length=1000, verbose_name="Filename"
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='kbiattachment', model_name="kbiattachment",
name='mime_type', name="mime_type",
field=models.CharField(blank=True, max_length=255, verbose_name='MIME Type'), field=models.CharField(
blank=True, max_length=255, verbose_name="MIME Type"
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='kbiattachment', model_name="kbiattachment",
name='size', name="size",
field=models.IntegerField(blank=True, help_text='Size of this file in bytes', verbose_name='Size'), field=models.IntegerField(
blank=True, help_text="Size of this file in bytes", verbose_name="Size"
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='kbitem', model_name="kbitem",
name='voted_by', name="voted_by",
field=models.ManyToManyField(related_name='votes', to=settings.AUTH_USER_MODEL), field=models.ManyToManyField(
related_name="votes", to=settings.AUTH_USER_MODEL
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='queue', model_name="queue",
name='enable_notifications_on_email_events', 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'), 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",
),
), ),
] ]

View File

@ -7,15 +7,20 @@ from helpdesk import settings as helpdesk_settings
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0027_auto_20200107_1221'), ("helpdesk", "0027_auto_20200107_1221"),
] + helpdesk_settings.HELPDESK_TEAMS_MIGRATION_DEPENDENCIES ] + helpdesk_settings.HELPDESK_TEAMS_MIGRATION_DEPENDENCIES
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='kbitem', model_name="kbitem",
name='team', 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'), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=helpdesk_settings.HELPDESK_TEAMS_MODEL,
verbose_name="Team",
),
), ),
] ]

View File

@ -4,15 +4,16 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0028_kbitem_team'), ("helpdesk", "0028_kbitem_team"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='kbcategory', model_name="kbcategory",
name='public', name="public",
field=models.BooleanField(default=True, verbose_name='Is KBCategory publicly visible?'), field=models.BooleanField(
default=True, verbose_name="Is KBCategory publicly visible?"
),
), ),
] ]

View File

@ -2,32 +2,44 @@
from django.db import migrations, models from django.db import migrations, models
def copy_title(apps, schema_editor): def copy_title(apps, schema_editor):
KBCategory = apps.get_model("helpdesk", "KBCategory") KBCategory = apps.get_model("helpdesk", "KBCategory")
KBCategory.objects.update(name=models.F('title')) KBCategory.objects.update(name=models.F("title"))
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0029_kbcategory_public'), ("helpdesk", "0029_kbcategory_public"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='kbcategory', model_name="kbcategory",
name='name', name="name",
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Name of the category'), field=models.CharField(
blank=True,
max_length=100,
null=True,
verbose_name="Name of the category",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='kbcategory', model_name="kbcategory",
name='title', name="title",
field=models.CharField(max_length=100, verbose_name='Title on knowledgebase page'), field=models.CharField(
max_length=100, verbose_name="Title on knowledgebase page"
),
), ),
migrations.RunPython(copy_title, migrations.RunPython.noop), migrations.RunPython(copy_title, migrations.RunPython.noop),
migrations.AlterField( migrations.AlterField(
model_name='kbcategory', model_name="kbcategory",
name='name', name="name",
field=models.CharField(blank=False, max_length=100, null=False, verbose_name='Name of the category'), field=models.CharField(
blank=False,
max_length=100,
null=False,
verbose_name="Name of the category",
),
), ),
] ]

View File

@ -4,19 +4,24 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0030_add_kbcategory_name'), ("helpdesk", "0030_add_kbcategory_name"),
] ]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='kbitem', name="kbitem",
options={'ordering': ('order', 'title'), 'verbose_name': 'Knowledge base item', 'verbose_name_plural': 'Knowledge base items'}, options={
"ordering": ("order", "title"),
"verbose_name": "Knowledge base item",
"verbose_name_plural": "Knowledge base items",
},
), ),
migrations.AddField( migrations.AddField(
model_name='kbitem', model_name="kbitem",
name='order', name="order",
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Order'), field=models.PositiveIntegerField(
blank=True, null=True, verbose_name="Order"
),
), ),
] ]

View File

@ -4,15 +4,16 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0031_auto_20200225_1440'), ("helpdesk", "0031_auto_20200225_1440"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='kbitem', model_name="kbitem",
name='enabled', name="enabled",
field=models.BooleanField(default=True, verbose_name='Enabled to display to users'), field=models.BooleanField(
default=True, verbose_name="Enabled to display to users"
),
), ),
] ]

View File

@ -5,15 +5,21 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0032_kbitem_enabled'), ("helpdesk", "0032_kbitem_enabled"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='ticket', model_name="ticket",
name='merged_to', 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'), 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",
),
), ),
] ]

View File

@ -7,10 +7,12 @@ def forwards_func(apps, schema_editor):
EmailTemplate = apps.get_model("helpdesk", "EmailTemplate") EmailTemplate = apps.get_model("helpdesk", "EmailTemplate")
db_alias = schema_editor.connection.alias db_alias = schema_editor.connection.alias
EmailTemplate.objects.using(db_alias).create( 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 id=EmailTemplate.objects.using(db_alias).order_by("-id").first().id + 1
template_name='merged', if EmailTemplate.objects.using(db_alias).first()
subject='(Merged)', else 1, # because PG sequences are not reset
heading='Ticket merged', template_name="merged",
subject="(Merged)",
heading="Ticket merged",
plain_text="""Hello, 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 }}. 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;">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>""", <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( EmailTemplate.objects.using(db_alias).create(
id=EmailTemplate.objects.using(db_alias).order_by('-id').first().id + 1, # because PG sequences are not reset id=EmailTemplate.objects.using(db_alias).order_by("-id").first().id
template_name='merged', + 1, # because PG sequences are not reset
subject='(Fusionné)', template_name="merged",
heading='Ticket Fusionné', subject="(Fusionné)",
heading="Ticket Fusionné",
plain_text="""Bonjour, 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 }}. 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;">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>""", <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): def reverse_func(apps, schema_editor):
EmailTemplate = apps.get_model("helpdesk", "EmailTemplate") EmailTemplate = apps.get_model("helpdesk", "EmailTemplate")
db_alias = schema_editor.connection.alias 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): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0033_ticket_merged_to'), ("helpdesk", "0033_ticket_merged_to"),
] ]
operations = [ operations = [

View File

@ -5,15 +5,18 @@ import helpdesk.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [
("helpdesk", "0034_create_email_template_for_merged"),
]
dependencies = [ operations = [
('helpdesk', '0034_create_email_template_for_merged'),
]
operations = [
migrations.AlterField( migrations.AlterField(
model_name='usersettings', model_name="usersettings",
name='email_on_ticket_change', 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?'), 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?",
),
), ),
] ]

View File

@ -6,20 +6,29 @@ import helpdesk.validators
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0035_alter_email_on_ticket_change'), ("helpdesk", "0035_alter_email_on_ticket_change"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='followupattachment', model_name="followupattachment",
name='file', name="file",
field=models.FileField(max_length=1000, upload_to=helpdesk.models.attachment_path, validators=[helpdesk.validators.validate_file_extension], verbose_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( migrations.AlterField(
model_name='kbiattachment', model_name="kbiattachment",
name='file', name="file",
field=models.FileField(max_length=1000, upload_to=helpdesk.models.attachment_path, validators=[helpdesk.validators.validate_file_extension], verbose_name='File'), field=models.FileField(
max_length=1000,
upload_to=helpdesk.models.attachment_path,
validators=[helpdesk.validators.validate_file_extension],
verbose_name="File",
),
), ),
] ]

View File

@ -4,15 +4,26 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0036_add_attachment_validator'), ("helpdesk", "0036_add_attachment_validator"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='queue', model_name="queue",
name='email_box_type', 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'), 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",
),
), ),
] ]

View File

@ -6,49 +6,107 @@ import helpdesk.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('helpdesk', '0037_alter_queue_email_box_type'), ("helpdesk", "0037_alter_queue_email_box_type"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Checklist', name="Checklist",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=100, verbose_name='Name')), "id",
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to='helpdesk.ticket', verbose_name='Ticket')), 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={ options={
'verbose_name': 'Checklist', "verbose_name": "Checklist",
'verbose_name_plural': 'Checklists', "verbose_name_plural": "Checklists",
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='ChecklistTemplate', name="ChecklistTemplate",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=100, verbose_name='Name')), "id",
('task_list', models.JSONField(validators=[helpdesk.models.is_a_list_without_empty_element], verbose_name='Task List')), 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={ options={
'verbose_name': 'Checklist Template', "verbose_name": "Checklist Template",
'verbose_name_plural': 'Checklist Templates', "verbose_name_plural": "Checklist Templates",
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='ChecklistTask', name="ChecklistTask",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('description', models.CharField(max_length=250, verbose_name='Description')), "id",
('completion_date', models.DateTimeField(blank=True, null=True, verbose_name='Completion Date')), models.AutoField(
('position', models.PositiveSmallIntegerField(db_index=True, verbose_name='Position')), auto_created=True,
('checklist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='helpdesk.checklist', verbose_name='Checklist')), 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={ options={
'verbose_name': 'Checklist Task', "verbose_name": "Checklist Task",
'verbose_name_plural': 'Checklist Tasks', "verbose_name_plural": "Checklist Tasks",
'ordering': ('position',), "ordering": ("position",),
}, },
), ),
] ]

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from django.db.models import Q, Max from django.db.models import Q, Max
from django.db.models import F, Window, Subquery, OuterRef 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. 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): def query_from_base64(b64data):
""" """
Converts base64-encoded bytes object back to a query dict object. Converts base64-encoded bytes object back to a query dict object.
""" """
query = {'search_string': ''} query = {"search_string": ""}
query.update(json.loads(b64decode(b64data).decode('utf-8'))) query.update(json.loads(b64decode(b64data).decode("utf-8")))
if query['search_string'] is None: if query["search_string"] is None:
query['search_string'] = '' query["search_string"] = ""
return query return query
def get_search_filter_args(search): def get_search_filter_args(search):
if not search: if not search:
return Q() return Q()
if search.startswith('queue:'): if search.startswith("queue:"):
return Q(queue__title__icontains=search[len('queue:'):]) return Q(queue__title__icontains=search[len("queue:") :])
if search.startswith('priority:'): if search.startswith("priority:"):
return Q(priority__icontains=search[len('priority:'):]) return Q(priority__icontains=search[len("priority:") :])
my_filter = Q() my_filter = Q()
for subsearch in search.split("OR"): for subsearch in search.split("OR"):
subsearch = subsearch.strip() subsearch = subsearch.strip()
if not subsearch: if not subsearch:
continue continue
my_filter = ( my_filter = (
filter | filter
Q(id__icontains=subsearch) | | Q(id__icontains=subsearch)
Q(title__icontains=subsearch) | | Q(title__icontains=subsearch)
Q(description__icontains=subsearch) | | Q(description__icontains=subsearch)
Q(priority__icontains=subsearch) | | Q(priority__icontains=subsearch)
Q(resolution__icontains=subsearch) | | Q(resolution__icontains=subsearch)
Q(submitter_email__icontains=subsearch) | | Q(submitter_email__icontains=subsearch)
Q(assigned_to__email__icontains=subsearch) | | Q(assigned_to__email__icontains=subsearch)
Q(ticketcustomfieldvalue__value__icontains=subsearch) | | Q(ticketcustomfieldvalue__value__icontains=subsearch)
Q(created__icontains=subsearch) | | Q(created__icontains=subsearch)
Q(due_date__icontains=subsearch) | Q(due_date__icontains=subsearch)
) )
return my_filter return my_filter
DATATABLES_ORDER_COLUMN_CHOICES = Choices( DATATABLES_ORDER_COLUMN_CHOICES = Choices(
('0', 'id'), ("0", "id"),
('1', 'title'), ("1", "title"),
('2', 'priority'), ("2", "priority"),
('3', 'queue'), ("3", "queue"),
('4', 'status'), ("4", "status"),
('5', 'created'), ("5", "created"),
('6', 'due_date'), ("6", "due_date"),
('7', 'assigned_to'), ("7", "assigned_to"),
('8', 'submitter_email'), ("8", "submitter_email"),
('9', 'last_followup'), ("9", "last_followup"),
# ('10', 'time_spent'), # ('10', 'time_spent'),
('11', 'kbitem'), ("11", "kbitem"),
) )
@ -78,22 +77,19 @@ def get_query_class():
def _get_query_class(): def _get_query_class():
return __Query__ return __Query__
return getattr(settings,
'HELPDESK_QUERY_CLASS', return getattr(settings, "HELPDESK_QUERY_CLASS", _get_query_class)()
_get_query_class)()
class __Query__: class __Query__:
def __init__(self, huser, base64query=None, query_params=None): def __init__(self, huser, base64query=None, query_params=None):
self.huser = huser self.huser = huser
self.params = query_params if query_params else query_from_base64( self.params = query_params if query_params else query_from_base64(base64query)
base64query) self.base64 = base64query if base64query else query_to_base64(query_params)
self.base64 = base64query if base64query else query_to_base64(
query_params)
self.result = None self.result = None
def get_search_filter_args(self): def get_search_filter_args(self):
search = self.params.get('search_string', '') search = self.params.get("search_string", "")
return get_search_filter_args(search) return get_search_filter_args(search)
def __run__(self, queryset): def __run__(self, queryset):
@ -112,15 +108,15 @@ class __Query__:
sorting: The name of the column to sort by sorting: The name of the column to sort by
""" """
q_args = [] q_args = []
value_filters = self.params.get('filtering', {}) value_filters = self.params.get("filtering", {})
null_filters = self.params.get('filtering_null', {}) null_filters = self.params.get("filtering_null", {})
if null_filters: if null_filters:
if value_filters: if value_filters:
# Check if any of the value value_filters are for the same field as the # 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 # ISNULL filter so that an OR filter can be set up
matched_null_keys = [] matched_null_keys = []
for null_key in null_filters: 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 matched_key = None
for val_key in value_filters: for val_key in value_filters:
if val_key.startswith(field_path): if val_key.startswith(field_path):
@ -140,10 +136,12 @@ class __Query__:
for null_key in matched_null_keys: for null_key in matched_null_keys:
del null_filters[null_key] del null_filters[null_key]
queryset = queryset.filter( queryset = queryset.filter(
*q_args, (Q(**value_filters) & Q(**null_filters)) & self.get_search_filter_args()) *q_args,
sorting = self.params.get('sorting', None) (Q(**value_filters) & Q(**null_filters)) & self.get_search_filter_args(),
)
sorting = self.params.get("sorting", None)
if sorting: if sorting:
sortreverse = self.params.get('sortreverse', None) sortreverse = self.params.get("sortreverse", None)
if sortreverse: if sortreverse:
sorting = "-%s" % sorting sorting = "-%s" % sorting
queryset = queryset.order_by(sorting) queryset = queryset.order_by(sorting)
@ -165,45 +163,49 @@ class __Query__:
to a Serializer called DatatablesTicketSerializer in serializers.py. to a Serializer called DatatablesTicketSerializer in serializers.py.
""" """
objects = self.get() objects = self.get()
order_by = '-created' order_by = "-created"
draw = int(kwargs.get('draw', [0])[0]) draw = int(kwargs.get("draw", [0])[0])
length = int(kwargs.get('length', [25])[0]) length = int(kwargs.get("length", [25])[0])
start = int(kwargs.get('start', [0])[0]) start = int(kwargs.get("start", [0])[0])
search_value = kwargs.get('search[value]', [""])[0] search_value = kwargs.get("search[value]", [""])[0]
order_column = kwargs.get('order[0][column]', ['5'])[0] order_column = kwargs.get("order[0][column]", ["5"])[0]
order = kwargs.get('order[0][dir]', ["asc"])[0] order = kwargs.get("order[0][dir]", ["asc"])[0]
order_column = DATATABLES_ORDER_COLUMN_CHOICES[order_column] order_column = DATATABLES_ORDER_COLUMN_CHOICES[order_column]
# django orm '-' -> desc # django orm '-' -> desc
if order == 'desc': if order == "desc":
order_column = '-' + order_column order_column = "-" + order_column
queryset = objects.annotate( queryset = objects.annotate(
last_followup=Subquery( last_followup=Subquery(
FollowUp.objects.order_by().annotate( FollowUp.objects.order_by()
.annotate(
last_followup=Window( last_followup=Window(
expression=Max("date"), expression=Max("date"),
partition_by=[F("ticket_id"),], partition_by=[
order_by="-date" F("ticket_id"),
],
order_by="-date",
) )
).filter( )
ticket_id=OuterRef("id") .filter(ticket_id=OuterRef("id"))
).values("last_followup").distinct() .values("last_followup")
.distinct()
) )
).order_by(order_by) ).order_by(order_by)
total = queryset.count() total = queryset.count()
if search_value: # Dead code currently if search_value: # Dead code currently
queryset = queryset.filter(get_search_filter_args(search_value)) queryset = queryset.filter(get_search_filter_args(search_value))
count = queryset.count() count = queryset.count()
queryset = queryset.order_by(order_column)[start:start + length] queryset = queryset.order_by(order_column)[start : start + length]
return { return {
'data': DatatablesTicketSerializer(queryset, many=True).data, "data": DatatablesTicketSerializer(queryset, many=True).data,
'recordsFiltered': count, "recordsFiltered": count,
'recordsTotal': total, "recordsTotal": total,
'draw': draw "draw": draw,
} }
def get_timeline_context(self): def get_timeline_context(self):
@ -212,33 +214,38 @@ class __Query__:
for ticket in self.get(): for ticket in self.get():
for followup in ticket.followup_set.all(): for followup in ticket.followup_set.all():
event = { event = {
'start_date': self.mk_timeline_date(followup.date), "start_date": self.mk_timeline_date(followup.date),
'text': { "text": {
'headline': ticket.title + ' - ' + followup.title, "headline": ticket.title + " - " + followup.title,
'text': ( "text": (
(escape(followup.comment) (
if followup.comment else _('No text')) escape(followup.comment)
+ if followup.comment
'<br/> <a href="%s" class="btn" role="button">%s</a>' else _("No text")
% )
(reverse('helpdesk:view', kwargs={ + '<br/> <a href="%s" class="btn" role="button">%s</a>'
'ticket_id': ticket.pk}), _("View ticket")) % (
reverse(
"helpdesk:view", kwargs={"ticket_id": ticket.pk}
),
_("View ticket"),
)
), ),
}, },
'group': _('Messages'), "group": _("Messages"),
} }
events.append(event) events.append(event)
return { return {
'events': events, "events": events,
} }
def mk_timeline_date(self, date): def mk_timeline_date(self, date):
return { return {
'year': date.year, "year": date.year,
'month': date.month, "month": date.month,
'day': date.day, "day": date.day,
'hour': date.hour, "hour": date.hour,
'minute': date.minute, "minute": date.minute,
'second': date.second, "second": date.second,
} }

View File

@ -15,6 +15,7 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
A serializer for the Ticket model, returns data in the format as required by 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. datatables for ticket_list.html. Called from staff.datatables_ticket_list.
""" """
ticket = serializers.SerializerMethodField() ticket = serializers.SerializerMethodField()
assigned_to = serializers.SerializerMethodField() assigned_to = serializers.SerializerMethodField()
submitter = serializers.SerializerMethodField() submitter = serializers.SerializerMethodField()
@ -30,9 +31,22 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Ticket model = Ticket
# fields = '__all__' # fields = '__all__'
fields = ('ticket', 'id', 'priority', 'title', 'queue', 'status', fields = (
'created', 'due_date', 'assigned_to', 'submitter', 'last_followup', "ticket",
'row_class', 'time_spent', 'kbitem') "id",
"priority",
"title",
"queue",
"status",
"created",
"due_date",
"assigned_to",
"submitter",
"last_followup",
"row_class",
"time_spent",
"kbitem",
)
def get_queue(self, obj): def get_queue(self, obj):
return {"title": obj.queue.title, "id": obj.queue.id} return {"title": obj.queue.title, "id": obj.queue.id}
@ -71,39 +85,46 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
def get_kbitem(self, obj): def get_kbitem(self, obj):
return obj.kbitem.title if obj.kbitem else "" return obj.kbitem.title if obj.kbitem else ""
def get_last_followup(self, obj): def get_last_followup(self, obj):
return obj.last_followup return obj.last_followup
class FollowUpAttachmentSerializer(serializers.ModelSerializer): class FollowUpAttachmentSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = FollowUpAttachment model = FollowUpAttachment
fields = ('id', 'followup', 'file', 'filename', 'mime_type', 'size') fields = ("id", "followup", "file", "filename", "mime_type", "size")
class FollowUpSerializer(serializers.ModelSerializer): class FollowUpSerializer(serializers.ModelSerializer):
followupattachment_set = FollowUpAttachmentSerializer( followupattachment_set = FollowUpAttachmentSerializer(many=True, read_only=True)
many=True, read_only=True)
attachments = serializers.ListField( attachments = serializers.ListField(
child=serializers.FileField(), child=serializers.FileField(), write_only=True, required=False
write_only=True,
required=False
) )
date = serializers.DateTimeField(read_only=True) date = serializers.DateTimeField(read_only=True)
class Meta: class Meta:
model = FollowUp model = FollowUp
fields = ( fields = (
'id', 'ticket', 'user', 'title', 'comment', 'public', 'new_status', "id",
'time_spent', 'attachments', 'followupattachment_set', 'date', 'message_id', "ticket",
"user",
"title",
"comment",
"public",
"new_status",
"time_spent",
"attachments",
"followupattachment_set",
"date",
"message_id",
) )
def create(self, validated_data): def create(self, validated_data):
if validated_data["user"]: if validated_data["user"]:
user = validated_data["user"] user = validated_data["user"]
else: else:
user = self.context['request'].user user = self.context["request"].user
return update_ticket( return update_ticket(
user=user, user=user,
ticket=validated_data["ticket"], ticket=validated_data["ticket"],
@ -121,12 +142,12 @@ class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = get_user_model() 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): def create(self, validated_data):
user = super(UserSerializer, self).create(validated_data) user = super(UserSerializer, self).create(validated_data)
user.is_active = True user.is_active = True
user.set_password(validated_data['password']) user.set_password(validated_data["password"])
user.save() user.save()
return user return user
@ -137,13 +158,14 @@ class BaseTicketSerializer(serializers.ModelSerializer):
# Add custom fields # Add custom fields
for field in CustomField.objects.all(): 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): class PublicTicketListingSerializer(BaseTicketSerializer):
""" """
A serializer to be used by the public API for listing tickets. Don't expose private fields here! A serializer to be used by the public API for listing tickets. Don't expose private fields here!
""" """
ticket = serializers.SerializerMethodField() ticket = serializers.SerializerMethodField()
submitter = serializers.SerializerMethodField() submitter = serializers.SerializerMethodField()
created = serializers.SerializerMethodField() created = serializers.SerializerMethodField()
@ -156,8 +178,18 @@ class PublicTicketListingSerializer(BaseTicketSerializer):
class Meta: class Meta:
model = Ticket model = Ticket
# fields = '__all__' # fields = '__all__'
fields = ('ticket', 'id', 'title', 'queue', 'status', fields = (
'created', 'due_date', 'submitter', 'kbitem', 'secret_key') "ticket",
"id",
"title",
"queue",
"status",
"created",
"due_date",
"submitter",
"kbitem",
"secret_key",
)
def get_queue(self, obj): def get_queue(self, obj):
return {"title": obj.queue.title, "id": obj.queue.id} return {"title": obj.queue.title, "id": obj.queue.id}
@ -188,29 +220,40 @@ class TicketSerializer(BaseTicketSerializer):
class Meta: class Meta:
model = Ticket model = Ticket
fields = ( fields = (
'id', 'queue', 'title', 'description', 'resolution', 'submitter_email', 'assigned_to', 'status', 'on_hold', "id",
'priority', 'due_date', 'merged_to', 'attachment', 'followup_set' "queue",
"title",
"description",
"resolution",
"submitter_email",
"assigned_to",
"status",
"on_hold",
"priority",
"due_date",
"merged_to",
"attachment",
"followup_set",
) )
def create(self, validated_data): def create(self, validated_data):
""" Use TicketForm to validate and create ticket """ """Use TicketForm to validate and create ticket"""
queues = HelpdeskUser(self.context['request'].user).get_queues() queues = HelpdeskUser(self.context["request"].user).get_queues()
queue_choices = [(q.id, q.title) for q in queues] queue_choices = [(q.id, q.title) for q in queues]
data = validated_data.copy() data = validated_data.copy()
data['body'] = data['description'] data["body"] = data["description"]
# TicketForm needs id for ForeignKey (not the instance themselves) # TicketForm needs id for ForeignKey (not the instance themselves)
data['queue'] = data['queue'].id data["queue"] = data["queue"].id
if data.get('assigned_to'): if data.get("assigned_to"):
data['assigned_to'] = data['assigned_to'].id data["assigned_to"] = data["assigned_to"].id
if data.get('merged_to'): if data.get("merged_to"):
data['merged_to'] = data['merged_to'].id data["merged_to"] = data["merged_to"].id
files = {'attachment': data.pop('attachment', None)} files = {"attachment": data.pop("attachment", None)}
ticket_form = TicketForm( ticket_form = TicketForm(data=data, files=files, queue_choices=queue_choices)
data=data, files=files, queue_choices=queue_choices)
if ticket_form.is_valid(): 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() ticket.set_custom_field_values()
return ticket return ticket

View File

@ -14,11 +14,11 @@ import sys
DEFAULT_USER_SETTINGS = { DEFAULT_USER_SETTINGS = {
'login_view_ticketlist': True, "login_view_ticketlist": True,
'email_on_ticket_change': True, "email_on_ticket_change": True,
'email_on_ticket_assign': True, "email_on_ticket_assign": True,
'tickets_per_page': 25, "tickets_per_page": 25,
'use_email_as_submitter': True, "use_email_as_submitter": True,
} }
try: try:
@ -33,8 +33,8 @@ HAS_TAG_SUPPORT = False
USE_TZ: bool = True USE_TZ: bool = True
# check for secure cookie support # check for secure cookie support
if os.environ.get('SECURE_PROXY_SSL_HEADER'): if os.environ.get("SECURE_PROXY_SSL_HEADER"):
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True
CSRF_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 "/"? # 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 = getattr(
'HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT', settings, "HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT", False
False) )
HELPDESK_PUBLIC_VIEW_PROTECTOR = getattr(settings, HELPDESK_PUBLIC_VIEW_PROTECTOR = getattr(
'HELPDESK_PUBLIC_VIEW_PROTECTOR', settings, "HELPDESK_PUBLIC_VIEW_PROTECTOR", lambda _: None
lambda _: None) )
HELPDESK_STAFF_VIEW_PROTECTOR = getattr(settings, HELPDESK_STAFF_VIEW_PROTECTOR = getattr(
'HELPDESK_STAFF_VIEW_PROTECTOR', settings, "HELPDESK_STAFF_VIEW_PROTECTOR", lambda _: None
lambda _: None) )
# Enable ticket and Email attachments # Enable ticket and Email attachments
# #
# Caution! Set this to False, unless you have secured access to # Caution! Set this to False, unless you have secured access to
# the uploaded files. Otherwise anyone on the Internet will be # the uploaded files. Otherwise anyone on the Internet will be
# able to download your ticket attachments. # able to download your ticket attachments.
HELPDESK_ENABLE_ATTACHMENTS = getattr(settings, HELPDESK_ENABLE_ATTACHMENTS = getattr(settings, "HELPDESK_ENABLE_ATTACHMENTS", True)
'HELPDESK_ENABLE_ATTACHMENTS',
True)
# Enable the Dependencies field on ticket view # Enable the Dependencies field on ticket view
HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(settings, HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(
'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET', settings, "HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET", True
True) )
# Enable the Time spent on field on ticket view # Enable the Time spent on field on ticket view
HELPDESK_ENABLE_TIME_SPENT_ON_TICKET = getattr(settings, HELPDESK_ENABLE_TIME_SPENT_ON_TICKET = getattr(
'HELPDESK_ENABLE_TIME_SPENT_ON_TICKET', settings, "HELPDESK_ENABLE_TIME_SPENT_ON_TICKET", True
True) )
# raises a 404 to anon users. It's like it was invisible # raises a 404 to anon users. It's like it was invisible
HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings, HELPDESK_ANON_ACCESS_RAISES_404 = getattr(
'HELPDESK_ANON_ACCESS_RAISES_404', settings, "HELPDESK_ANON_ACCESS_RAISES_404", False
False) )
# Disable Timeline on ticket list # Disable Timeline on ticket list
HELPDESK_TICKETS_TIMELINE_ENABLED = getattr( 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 # show extended navigation by default, to all users, irrespective of staff
# status? # status?
HELPDESK_NAVIGATION_ENABLED = getattr( HELPDESK_NAVIGATION_ENABLED = getattr(settings, "HELPDESK_NAVIGATION_ENABLED", False)
settings, 'HELPDESK_NAVIGATION_ENABLED', False)
# use public CDNs to serve jquery and other javascript by default? # use public CDNs to serve jquery and other javascript by default?
# otherwise, use built-in static copy # 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? # show dropdown list of languages that ticket comments can be translated into?
HELPDESK_TRANSLATE_TICKET_COMMENTS = getattr(settings, HELPDESK_TRANSLATE_TICKET_COMMENTS = getattr(
'HELPDESK_TRANSLATE_TICKET_COMMENTS', settings, "HELPDESK_TRANSLATE_TICKET_COMMENTS", False
False) )
# list of languages to offer. if set to false, # list of languages to offer. if set to false,
# all default google translate languages will be shown. # all default google translate languages will be shown.
HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG = getattr(settings, HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG = getattr(
'HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG', settings,
["en", "de", "es", "fr", "it", "ru"]) "HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG",
["en", "de", "es", "fr", "it", "ru"],
)
# show link to 'change password' on 'User Settings' page? # show link to 'change password' on 'User Settings' page?
HELPDESK_SHOW_CHANGE_PASSWORD = getattr( 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. # 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? # 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 = getattr(
'HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE', settings, "HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE", False
False) )
# URL schemes that are allowed within links # URL schemes that are allowed within links
ALLOWED_URL_SCHEMES = getattr(settings, 'ALLOWED_URL_SCHEMES', ( ALLOWED_URL_SCHEMES = getattr(
'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp', settings,
)) "ALLOWED_URL_SCHEMES",
(
"file",
"ftp",
"ftps",
"http",
"https",
"irc",
"mailto",
"sftp",
"ssh",
"tel",
"telnet",
"tftp",
"vnc",
"xmpp",
),
)
# Ticket status choices # Ticket status choices
OPEN_STATUS = getattr(settings, 'HELPDESK_TICKET_OPEN_STATUS', 1) OPEN_STATUS = getattr(settings, "HELPDESK_TICKET_OPEN_STATUS", 1)
REOPENED_STATUS = getattr(settings, 'HELPDESK_TICKET_REOPENED_STATUS', 2) REOPENED_STATUS = getattr(settings, "HELPDESK_TICKET_REOPENED_STATUS", 2)
RESOLVED_STATUS = getattr(settings, 'HELPDESK_TICKET_RESOLVED_STATUS', 3) RESOLVED_STATUS = getattr(settings, "HELPDESK_TICKET_RESOLVED_STATUS", 3)
CLOSED_STATUS = getattr(settings, 'HELPDESK_TICKET_CLOSED_STATUS', 4) CLOSED_STATUS = getattr(settings, "HELPDESK_TICKET_CLOSED_STATUS", 4)
DUPLICATE_STATUS = getattr(settings, 'HELPDESK_TICKET_DUPLICATE_STATUS', 5) DUPLICATE_STATUS = getattr(settings, "HELPDESK_TICKET_DUPLICATE_STATUS", 5)
DEFAULT_TICKET_STATUS_CHOICES = ( DEFAULT_TICKET_STATUS_CHOICES = (
(OPEN_STATUS, _('Open')), (OPEN_STATUS, _("Open")),
(REOPENED_STATUS, _('Reopened')), (REOPENED_STATUS, _("Reopened")),
(RESOLVED_STATUS, _('Resolved')), (RESOLVED_STATUS, _("Resolved")),
(CLOSED_STATUS, _('Closed')), (CLOSED_STATUS, _("Closed")),
(DUPLICATE_STATUS, _('Duplicate')), (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" # List of status choices considered as "open"
DEFAULT_TICKET_OPEN_STATUSES = (OPEN_STATUS, REOPENED_STATUS) DEFAULT_TICKET_OPEN_STATUSES = (OPEN_STATUS, REOPENED_STATUS)
TICKET_OPEN_STATUSES = getattr(settings, TICKET_OPEN_STATUSES = getattr(
'HELPDESK_TICKET_OPEN_STATUSES', settings, "HELPDESK_TICKET_OPEN_STATUSES", DEFAULT_TICKET_OPEN_STATUSES
DEFAULT_TICKET_OPEN_STATUSES) )
# New status list choices depending on current ticket status # New status list choices depending on current ticket status
DEFAULT_TICKET_STATUS_CHOICES_FLOW = { DEFAULT_TICKET_STATUS_CHOICES_FLOW = {
OPEN_STATUS: (OPEN_STATUS, RESOLVED_STATUS, CLOSED_STATUS, DUPLICATE_STATUS,), OPEN_STATUS: (
REOPENED_STATUS: (REOPENED_STATUS, RESOLVED_STATUS, CLOSED_STATUS, DUPLICATE_STATUS,), OPEN_STATUS,
RESOLVED_STATUS: (REOPENED_STATUS, RESOLVED_STATUS, CLOSED_STATUS,), RESOLVED_STATUS,
CLOSED_STATUS: (REOPENED_STATUS, CLOSED_STATUS,), CLOSED_STATUS,
DUPLICATE_STATUS: (REOPENED_STATUS, DUPLICATE_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, TICKET_STATUS_CHOICES_FLOW = getattr(
'HELPDESK_TICKET_STATUS_CHOICES_FLOW', settings, "HELPDESK_TICKET_STATUS_CHOICES_FLOW", DEFAULT_TICKET_STATUS_CHOICES_FLOW
DEFAULT_TICKET_STATUS_CHOICES_FLOW) )
# Ticket priority choices # Ticket priority choices
DEFAULT_TICKET_PRIORITY_CHOICES = ( DEFAULT_TICKET_PRIORITY_CHOICES = (
(1, _('1. Critical')), (1, _("1. Critical")),
(2, _('2. High')), (2, _("2. High")),
(3, _('3. Normal')), (3, _("3. Normal")),
(4, _('4. Low')), (4, _("4. Low")),
(5, _('5. Very 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 # Follow-ups automatic time_spent calculation
FOLLOWUP_TIME_SPENT_AUTO = getattr(settings, FOLLOWUP_TIME_SPENT_AUTO = getattr(settings, "HELPDESK_FOLLOWUP_TIME_SPENT_AUTO", False)
'HELPDESK_FOLLOWUP_TIME_SPENT_AUTO',
False)
# Calculate time_spent according to open hours # Calculate time_spent according to open hours
FOLLOWUP_TIME_SPENT_OPENING_HOURS = getattr(settings, FOLLOWUP_TIME_SPENT_OPENING_HOURS = getattr(
'HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS', settings, "HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS", {}
{}) )
# Holidays don't count for time_spent calculation # Holidays don't count for time_spent calculation
FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = getattr(settings, FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = getattr(
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS', settings, "HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS", ()
()) )
# Time doesn't count for listed ticket statuses # Time doesn't count for listed ticket statuses
FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = getattr(settings, FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = getattr(
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES', settings, "HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES", ()
()) )
# Time doesn't count for listed queues slugs # Time doesn't count for listed queues slugs
FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = getattr(settings, FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = getattr(
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES', settings, "HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES", ()
()) )
############################ ############################
# options for public pages # # options for public pages #
############################ ############################
# show 'view a ticket' section on public page? # show 'view a ticket' section on public page?
HELPDESK_VIEW_A_TICKET_PUBLIC = getattr( HELPDESK_VIEW_A_TICKET_PUBLIC = getattr(settings, "HELPDESK_VIEW_A_TICKET_PUBLIC", True)
settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC', True)
# show 'submit a ticket' section on public page? # show 'submit a ticket' section on public page?
HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr( 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) # change that to custom class to have extra fields or validation (like captcha)
HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr( HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr(
settings, settings, "HELPDESK_PUBLIC_TICKET_FORM_CLASS", "helpdesk.forms.PublicTicketForm"
"HELPDESK_PUBLIC_TICKET_FORM_CLASS",
"helpdesk.forms.PublicTicketForm"
) )
# Custom fields constants # Custom fields constants
CUSTOMFIELD_TO_FIELD_DICT = { CUSTOMFIELD_TO_FIELD_DICT = {
'boolean': forms.BooleanField, "boolean": forms.BooleanField,
'date': forms.DateField, "date": forms.DateField,
'time': forms.TimeField, "time": forms.TimeField,
'datetime': forms.DateTimeField, "datetime": forms.DateTimeField,
'email': forms.EmailField, "email": forms.EmailField,
'url': forms.URLField, "url": forms.URLField,
'ipaddress': forms.GenericIPAddressField, "ipaddress": forms.GenericIPAddressField,
'slug': forms.SlugField, "slug": forms.SlugField,
} }
CUSTOMFIELD_DATE_FORMAT = "%Y-%m-%d" CUSTOMFIELD_DATE_FORMAT = "%Y-%m-%d"
CUSTOMFIELD_TIME_FORMAT = "%H:%M:%S" 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 ''' """ options for update_ticket views """
# allow non-staff users to interact with tickets? # allow non-staff users to interact with tickets?
# can be True/False or a callable accepting the active user and returning # can be True/False or a callable accepting the active user and returning
# True if they must be considered helpdesk staff # True if they must be considered helpdesk staff
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr( HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(
settings, 'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE', False) 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)): )
if not (
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE in (True, False)
or callable(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE)
):
warnings.warn( warnings.warn(
"HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE should be set to either True/False or a callable.", "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. # show edit buttons in ticket follow ups.
HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(settings, HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(
'HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP', settings, "HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP", True
True) )
HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST = getattr(settings, HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST = getattr(
'HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST', settings, "HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST", []
[]) )
# show delete buttons in ticket follow ups if user is 'superuser' # show delete buttons in ticket follow ups if user is 'superuser'
HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP = getattr( 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 # make all updates public by default? this will hide the 'is this update
# public' checkbox # public' checkbox
HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr( 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 # only show staff users in ticket owner drop-downs
HELPDESK_STAFF_ONLY_TICKET_OWNERS = getattr( 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 # only show staff users in ticket cc drop-down
HELPDESK_STAFF_ONLY_TICKET_CC = getattr( 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. # allow the subject to have a configurable template.
HELPDESK_EMAIL_SUBJECT_TEMPLATE = getattr( HELPDESK_EMAIL_SUBJECT_TEMPLATE = getattr(
settings, 'HELPDESK_EMAIL_SUBJECT_TEMPLATE', settings,
"{{ ticket.ticket }} {{ ticket.title|safe }} %(subject)s") "HELPDESK_EMAIL_SUBJECT_TEMPLATE",
"{{ ticket.ticket }} {{ ticket.title|safe }} %(subject)s",
)
# since django-helpdesk may not work correctly without the ticket ID # since django-helpdesk may not work correctly without the ticket ID
# in the subject, let's do a check for it quick: # in the subject, let's do a check for it quick:
if HELPDESK_EMAIL_SUBJECT_TEMPLATE.find("ticket.ticket") < 0: 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 # default fallback locale when queue locale not found
HELPDESK_EMAIL_FALLBACK_LOCALE = getattr( HELPDESK_EMAIL_FALLBACK_LOCALE = getattr(
settings, 'HELPDESK_EMAIL_FALLBACK_LOCALE', 'en') settings, "HELPDESK_EMAIL_FALLBACK_LOCALE", "en"
)
# default maximum email attachment size, in bytes # default maximum email attachment size, in bytes
# only attachments smaller than this size will be sent via email # only attachments smaller than this size will be sent via email
HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr( 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? # hide the 'assigned to' / 'Case owner' field from the 'create_ticket' view?
HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr( 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 # default Queue email submission settings
QUEUE_EMAIL_BOX_TYPE = getattr(settings, 'QUEUE_EMAIL_BOX_TYPE', 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_SSL = getattr(settings, "QUEUE_EMAIL_BOX_SSL", None)
QUEUE_EMAIL_BOX_HOST = getattr(settings, 'QUEUE_EMAIL_BOX_HOST', 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_USER = getattr(settings, "QUEUE_EMAIL_BOX_USER", None)
QUEUE_EMAIL_BOX_PASSWORD = getattr(settings, 'QUEUE_EMAIL_BOX_PASSWORD', 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) # only process emails with a valid tracking ID? (throws away all other mail)
QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr( QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr(settings, "QUEUE_EMAIL_BOX_UPDATE_ONLY", False)
settings, 'QUEUE_EMAIL_BOX_UPDATE_ONLY', False)
# only allow users to access queues that they are members of? # only allow users to access queues that they are members of?
HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = getattr( 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 # use https in the email links
HELPDESK_USE_HTTPS_IN_EMAIL_LINK = getattr( 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 # 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: if HELPDESK_TEAMS_MODE_ENABLED:
HELPDESK_TEAMS_MODEL = getattr( HELPDESK_TEAMS_MODEL = getattr(settings, "HELPDESK_TEAMS_MODEL", "pinax_teams.Team")
settings, 'HELPDESK_TEAMS_MODEL', 'pinax_teams.Team') HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = getattr(
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = getattr(settings, 'HELPDESK_TEAMS_MIGRATION_DEPENDENCIES', [ settings,
('pinax_teams', '0004_auto_20170511_0856')]) "HELPDESK_TEAMS_MIGRATION_DEPENDENCIES",
[("pinax_teams", "0004_auto_20170511_0856")],
)
HELPDESK_KBITEM_TEAM_GETTER = getattr( HELPDESK_KBITEM_TEAM_GETTER = getattr(
settings, 'HELPDESK_KBITEM_TEAM_GETTER', lambda kbitem: kbitem.team) settings, "HELPDESK_KBITEM_TEAM_GETTER", lambda kbitem: kbitem.team
)
else: else:
HELPDESK_TEAMS_MODEL = settings.AUTH_USER_MODEL HELPDESK_TEAMS_MODEL = settings.AUTH_USER_MODEL
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = [] HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = []
@ -343,35 +394,38 @@ else:
# show knowledgebase links? # show knowledgebase links?
# If Teams mode is enabled then it has to be on # 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 # 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 # Useful if you get forwards dropped from them while they are useful part
# of request # of request
HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr( 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 # 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 is quite noisy but very helpful for complicated markup, forwards and so on
# (which gets stripped/corrupted otherwise) # (which gets stripped/corrupted otherwise)
HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr( 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 # # email OAUTH #
####################### #######################
HELPDESK_OAUTH = getattr( HELPDESK_OAUTH = getattr(
settings, 'HELPDESK_OAUTH', { settings,
"token_url": "", "HELPDESK_OAUTH",
"client_id": "", {"token_url": "", "client_id": "", "secret": "", "scope": [""]},
"secret": "",
"scope": [""]
}
) )
# Set Debug Logging Level for IMAP Services. Default to '0' for No Debugging # 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 # # 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) # Attachment directories should be created with permission 755 (rwxr-xr-x)
# Override it in your own Django settings.py # 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: if HELPDESK_VALID_EXTENSIONS:
# Print to stderr # 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: 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(): 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: 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(): 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: 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)

View File

@ -4,4 +4,4 @@ import django.dispatch
new_ticket_done = django.dispatch.Signal() new_ticket_done = django.dispatch.Signal()
# create a signal for ticket_update view # create a signal for ticket_update view
update_ticket_done = django.dispatch.Signal() update_ticket_done = django.dispatch.Signal()

View File

@ -1,4 +1,3 @@
from django.conf import settings from django.conf import settings
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
import logging import logging
@ -6,17 +5,19 @@ import os
from smtplib import SMTPException from smtplib import SMTPException
logger = logging.getLogger('helpdesk') logger = logging.getLogger("helpdesk")
def send_templated_mail(template_name, def send_templated_mail(
context, template_name,
recipients, context,
sender=None, recipients,
bcc=None, sender=None,
fail_silently=False, bcc=None,
files=None, fail_silently=False,
extra_headers=None): files=None,
extra_headers=None,
):
""" """
send_templated_mail() is a wrapper around Django's e-mail routines that 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 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.core.mail import EmailMultiAlternatives
from django.template import engines from django.template import engines
from_string = engines['django'].from_string
from_string = engines["django"].from_string
from helpdesk.models import EmailTemplate 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 {} 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: try:
t = EmailTemplate.objects.get( t = EmailTemplate.objects.get(
template_name__iexact=template_name, locale=locale) template_name__iexact=template_name, locale=locale
)
except EmailTemplate.DoesNotExist: except EmailTemplate.DoesNotExist:
try: try:
t = EmailTemplate.objects.get( t = EmailTemplate.objects.get(
template_name__iexact=template_name, locale__isnull=True) template_name__iexact=template_name, locale__isnull=True
)
except EmailTemplate.DoesNotExist: except EmailTemplate.DoesNotExist:
logger.warning( logger.warning('template "%s" does not exist, no mail sent', template_name)
'template "%s" does not exist, no mail sent', template_name)
return # just ignore if template doesn't exist return # just ignore if template doesn't exist
subject_part = from_string( subject_part = (
HELPDESK_EMAIL_SUBJECT_TEMPLATE % { from_string(HELPDESK_EMAIL_SUBJECT_TEMPLATE % {"subject": t.subject})
"subject": t.subject .render(context)
}).render(context).replace('\n', '').replace('\r', '') .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( text_part = from_string(
"%s\n\n{%% include '%s' %%}" % (t.plain_text, footer_file) "%s\n\n{%% include '%s' %%}" % (t.plain_text, footer_file)
).render(context) ).render(context)
email_html_base_file = os.path.join( email_html_base_file = os.path.join("helpdesk", locale, "email_html_base.html")
'helpdesk', locale, 'email_html_base.html')
# keep new lines in html emails # keep new lines in html emails
if 'comment' in context: if "comment" in context:
context['comment'] = mark_safe( context["comment"] = mark_safe(context["comment"].replace("\r\n", "<br>"))
context['comment'].replace('\r\n', '<br>'))
html_part = from_string( html_part = from_string(
"{%% extends '%s' %%}" "{%% extends '%s' %%}"
"{%% block title %%}%s{%% endblock %%}" "{%% block title %%}%s{%% endblock %%}"
"{%% block content %%}%s{%% endblock %%}" % "{%% block content %%}%s{%% endblock %%}"
(email_html_base_file, t.heading, t.html) % (email_html_base_file, t.heading, t.html)
).render(context) ).render(context)
if isinstance(recipients, str): if isinstance(recipients, str):
if recipients.find(','): if recipients.find(","):
recipients = recipients.split(',') recipients = recipients.split(",")
elif type(recipients) is not list: elif type(recipients) is not list:
recipients = [recipients] recipients = [recipients]
msg = EmailMultiAlternatives(subject_part, text_part, msg = EmailMultiAlternatives(
sender or settings.DEFAULT_FROM_EMAIL, subject_part,
recipients, bcc=bcc, text_part,
headers=headers) sender or settings.DEFAULT_FROM_EMAIL,
recipients,
bcc=bcc,
headers=headers,
)
msg.attach_alternative(html_part, "text/html") msg.attach_alternative(html_part, "text/html")
if files: if files:
for filename, filefield in files: for filename, filefield in files:
filefield.open('rb') filefield.open("rb")
content = filefield.read() content = filefield.read()
msg.attach(filename, content) msg.attach(filename, content)
filefield.close() filefield.close()
logger.debug('Sending email to: {!r}'.format(recipients)) logger.debug("Sending email to: {!r}".format(recipients))
try: try:
return msg.send() return msg.send()
except SMTPException as e: except SMTPException as e:
logger.exception( logger.exception(
'SMTPException raised while sending email to {}'.format(recipients)) "SMTPException raised while sending email to {}".format(recipients)
)
if not fail_silently: if not fail_silently:
raise e raise e
return 0 return 0

View File

@ -14,10 +14,9 @@ logger = logging.getLogger(__name__)
register = Library() register = Library()
@register.filter(name='is_helpdesk_staff') @register.filter(name="is_helpdesk_staff")
def helpdesk_staff(user): def helpdesk_staff(user):
try: try:
return is_helpdesk_staff(user) return is_helpdesk_staff(user)
except Exception: except Exception:
logger.exception( logger.exception("'helpdesk_staff' template tag (django-helpdesk) crashed")
"'helpdesk_staff' template tag (django-helpdesk) crashed")

View File

@ -2,7 +2,11 @@ from datetime import datetime
from django.conf import settings from django.conf import settings
from django.template import Library from django.template import Library
from django.template.defaultfilters import date as date_filter 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() register = Library()
@ -10,7 +14,7 @@ register = Library()
@register.filter @register.filter
def get(value, arg, default=None): def get(value, arg, default=None):
""" Call the dictionary get function """ """Call the dictionary get function"""
return value.get(arg, default) 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 :return: String - reformatted to default datetime, date, or time string if received in one of the expected formats
""" """
try: try:
new_value = date_filter(datetime.strptime( new_value = date_filter(
value, CUSTOMFIELD_DATETIME_FORMAT), settings.DATETIME_FORMAT) datetime.strptime(value, CUSTOMFIELD_DATETIME_FORMAT),
settings.DATETIME_FORMAT,
)
except (TypeError, ValueError): except (TypeError, ValueError):
try: try:
new_value = date_filter(datetime.strptime( new_value = date_filter(
value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT) datetime.strptime(value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT
)
except (TypeError, ValueError): except (TypeError, ValueError):
try: try:
new_value = date_filter(datetime.strptime( new_value = date_filter(
value, CUSTOMFIELD_TIME_FORMAT), settings.TIME_FORMAT) datetime.strptime(value, CUSTOMFIELD_TIME_FORMAT),
settings.TIME_FORMAT,
)
except (TypeError, ValueError): except (TypeError, ValueError):
# If NoneType return empty string, else return original value # If NoneType return empty string, else return original value
new_value = "" if value is None else value new_value = "" if value is None else value

View File

@ -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 templatetags/load_helpdesk_settings.py - returns the settings as defined in
django-helpdesk/helpdesk/settings.py django-helpdesk/helpdesk/settings.py
""" """
from django.template import Library from django.template import Library
from helpdesk import settings as helpdesk_settings_config from helpdesk import settings as helpdesk_settings_config
@ -13,11 +14,14 @@ def load_helpdesk_settings(request):
return helpdesk_settings_config return helpdesk_settings_config
except Exception as e: except Exception as e:
import sys 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) print(e, file=sys.stderr)
return '' return ""
register = Library() register = Library()
register.filter('load_helpdesk_settings', load_helpdesk_settings) register.filter("load_helpdesk_settings", load_helpdesk_settings)

View File

@ -5,6 +5,7 @@ templatetags/saved_queries.py - This template tag returns previously saved
queries. Therefore you don't need to modify queries. Therefore you don't need to modify
any views. any views.
""" """
from django import template from django import template
from django.db.models import Q from django.db.models import Q
from helpdesk.models import SavedSearch from helpdesk.models import SavedSearch
@ -23,7 +24,10 @@ def saved_queries(user):
return user_saved_queries return user_saved_queries
except Exception as e: except Exception as e:
import sys 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) print(e, file=sys.stderr)
return '' return ""

View File

@ -19,7 +19,7 @@ import re
def num_to_link(text): def num_to_link(text):
if text == '': if text == "":
return text return text
matches = [] matches = []
@ -28,7 +28,7 @@ def num_to_link(text):
for match in reversed(matches): for match in reversed(matches):
number = match.groups()[0] number = match.groups()[0]
url = reverse('helpdesk:view', args=[number]) url = reverse("helpdesk:view", args=[number])
try: try:
ticket = Ticket.objects.get(id=number) ticket = Ticket.objects.get(id=number)
except Ticket.DoesNotExist: except Ticket.DoesNotExist:
@ -36,8 +36,16 @@ def num_to_link(text):
if ticket: if ticket:
style = ticket.get_status_display() style = ticket.get_status_display()
text = "%s <a href='%s' class='ticket_link_status ticket_link_status_%s'>#%s</a>%s" % ( text = (
text[:match.start() + 1], url, style, match.groups()[0], text[match.end():]) "%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) return mark_safe(text)

View File

@ -20,9 +20,7 @@ def user_admin_url(action):
except AttributeError: # module_name alias removed in django 1.8 except AttributeError: # module_name alias removed in django 1.8
model_name = user._meta.model_name.lower() model_name = user._meta.model_name.lower()
return 'admin:%s_%s_%s' % ( return "admin:%s_%s_%s" % (user._meta.app_label, model_name, action)
user._meta.app_label, model_name,
action)
register = template.Library() register = template.Library()

View File

@ -8,16 +8,15 @@ import sys
User = get_user_model() User = get_user_model()
def get_user(username='helpdesk.staff', def get_user(
password='password', username="helpdesk.staff", password="password", is_staff=False, is_superuser=False
is_staff=False, ):
is_superuser=False):
try: try:
user = User.objects.get(username=username) user = User.objects.get(username=username)
except User.DoesNotExist: except User.DoesNotExist:
user = User.objects.create_user(username=username, user = User.objects.create_user(
password=password, username=username, password=password, email="%s@example.com" % username
email='%s@example.com' % username) )
user.is_staff = is_staff user.is_staff = is_staff
user.is_superuser = is_superuser user.is_superuser = is_superuser
user.save() user.save()
@ -32,7 +31,6 @@ def get_staff_user():
def reload_urlconf(urlconf=None): def reload_urlconf(urlconf=None):
from importlib import reload from importlib import reload
if urlconf is None: if urlconf is None:
@ -47,25 +45,29 @@ def reload_urlconf(urlconf=None):
reload(sys.modules[urlconf]) reload(sys.modules[urlconf])
from django.urls import clear_url_caches from django.urls import clear_url_caches
clear_url_caches() clear_url_caches()
def create_ticket(**kwargs): def create_ticket(**kwargs):
q = kwargs.get('queue', None) q = kwargs.get("queue", None)
if q is None: if q is None:
try: try:
q = Queue.objects.all()[0] q = Queue.objects.all()[0]
except IndexError: except IndexError:
q = Queue.objects.create(title='Test Q', slug='test', ) q = Queue.objects.create(
title="Test Q",
slug="test",
)
data = { data = {
'title': "I wish to register a complaint", "title": "I wish to register a complaint",
'queue': q, "queue": q,
} }
data.update(kwargs) data.update(kwargs)
return Ticket.objects.create(**data) return Ticket.objects.create(**data)
HELPDESK_URLCONF = 'helpdesk.urls' HELPDESK_URLCONF = "helpdesk.urls"
def print_response(response, stdout=False): def print_response(response, stdout=False):

View File

@ -1,4 +1,3 @@
import base64 import base64
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime from datetime import datetime
@ -13,7 +12,7 @@ from rest_framework.status import (
HTTP_201_CREATED, HTTP_201_CREATED,
HTTP_204_NO_CONTENT, HTTP_204_NO_CONTENT,
HTTP_400_BAD_REQUEST, HTTP_400_BAD_REQUEST,
HTTP_403_FORBIDDEN HTTP_403_FORBIDDEN,
) )
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
@ -24,106 +23,124 @@ class TicketTest(APITestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
cls.queue = Queue.objects.create( cls.queue = Queue.objects.create(
title='Test Queue', title="Test Queue",
slug='test-queue', slug="test-queue",
) )
def test_create_api_ticket_not_authenticated_user(self): 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) self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
def test_create_api_ticket_authenticated_non_staff_user(self): 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) 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) self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
def test_create_api_ticket_no_data(self): 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) 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.status_code, HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, { self.assertEqual(
'queue': [ErrorDetail(string='This field is required.', code='required')], response.data,
'title': [ErrorDetail(string='This field is required.', code='required')] {
}) "queue": [
ErrorDetail(string="This field is required.", code="required")
],
"title": [
ErrorDetail(string="This field is required.", code="required")
],
},
)
self.assertFalse(Ticket.objects.exists()) self.assertFalse(Ticket.objects.exists())
def test_create_api_ticket_wrong_date_format(self): 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) self.client.force_authenticate(staff_user)
response = self.client.post('/api/tickets/', { response = self.client.post(
'queue': self.queue.id, "/api/tickets/",
'title': 'Test title', {
'due_date': 'monday, 1st of may 2022' "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.status_code, HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, { self.assertEqual(
'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')] 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()) self.assertFalse(Ticket.objects.exists())
def test_create_api_ticket_authenticated_staff_user(self): 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) self.client.force_authenticate(staff_user)
response = self.client.post('/api/tickets/', { response = self.client.post(
'queue': self.queue.id, "/api/tickets/",
'title': 'Test title', {
'description': 'Test description\nMulti lines', "queue": self.queue.id,
'submitter_email': 'test@mail.com', "title": "Test title",
'priority': 4 "description": "Test description\nMulti lines",
}) "submitter_email": "test@mail.com",
"priority": 4,
},
)
self.assertEqual(response.status_code, HTTP_201_CREATED) self.assertEqual(response.status_code, HTTP_201_CREATED)
created_ticket = Ticket.objects.get() created_ticket = Ticket.objects.get()
self.assertEqual(created_ticket.title, 'Test title') self.assertEqual(created_ticket.title, "Test title")
self.assertEqual(created_ticket.description, self.assertEqual(created_ticket.description, "Test description\nMulti lines")
'Test description\nMulti lines') self.assertEqual(created_ticket.submitter_email, "test@mail.com")
self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
self.assertEqual(created_ticket.priority, 4) self.assertEqual(created_ticket.priority, 4)
self.assertEqual(created_ticket.followup_set.count(), 1) self.assertEqual(created_ticket.followup_set.count(), 1)
def test_create_api_ticket_with_basic_auth(self): def test_create_api_ticket_with_basic_auth(self):
username = 'admin' username = "admin"
password = 'admin' password = "admin"
User.objects.create_user( User.objects.create_user(username=username, password=password, is_staff=True)
username=username, password=password, is_staff=True)
test_user = User.objects.create_user(username='test') test_user = User.objects.create_user(username="test")
merge_ticket = Ticket.objects.create( merge_ticket = Ticket.objects.create(queue=self.queue, title="merge ticket")
queue=self.queue, title='merge ticket')
# Generate base64 credentials string # Generate base64 credentials string
credentials = f"{username}:{password}" credentials = f"{username}:{password}"
base64_credentials = base64.b64encode(credentials.encode( base64_credentials = base64.b64encode(
HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING) credentials.encode(HTTP_HEADER_ENCODING)
).decode(HTTP_HEADER_ENCODING)
self.client.credentials( self.client.credentials(HTTP_AUTHORIZATION=f"Basic {base64_credentials}")
HTTP_AUTHORIZATION=f"Basic {base64_credentials}")
response = self.client.post( response = self.client.post(
'/api/tickets/', "/api/tickets/",
{ {
'queue': self.queue.id, "queue": self.queue.id,
'title': 'Title', "title": "Title",
'description': 'Description', "description": "Description",
'resolution': 'Resolution', "resolution": "Resolution",
'assigned_to': test_user.id, "assigned_to": test_user.id,
'submitter_email': 'test@mail.com', "submitter_email": "test@mail.com",
'status': Ticket.RESOLVED_STATUS, "status": Ticket.RESOLVED_STATUS,
'priority': 1, "priority": 1,
'on_hold': True, "on_hold": True,
'due_date': self.due_date, "due_date": self.due_date,
'merged_to': merge_ticket.id "merged_to": merge_ticket.id,
} },
) )
self.assertEqual(response.status_code, HTTP_201_CREATED) self.assertEqual(response.status_code, HTTP_201_CREATED)
created_ticket = Ticket.objects.last() created_ticket = Ticket.objects.last()
self.assertEqual(created_ticket.title, 'Title') self.assertEqual(created_ticket.title, "Title")
self.assertEqual(created_ticket.description, 'Description') self.assertEqual(created_ticket.description, "Description")
# resolution can not be set on creation # resolution can not be set on creation
self.assertIsNone(created_ticket.resolution) self.assertIsNone(created_ticket.resolution)
self.assertEqual(created_ticket.assigned_to, test_user) 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) self.assertEqual(created_ticket.priority, 1)
# on_hold is False on creation # on_hold is False on creation
self.assertFalse(created_ticket.on_hold) self.assertFalse(created_ticket.on_hold)
@ -134,39 +151,37 @@ class TicketTest(APITestCase):
self.assertIsNone(created_ticket.merged_to) self.assertIsNone(created_ticket.merged_to)
def test_edit_api_ticket(self): def test_edit_api_ticket(self):
staff_user = User.objects.create_user(username='admin', is_staff=True) staff_user = User.objects.create_user(username="admin", is_staff=True)
test_ticket = Ticket.objects.create( test_ticket = Ticket.objects.create(queue=self.queue, title="Test ticket")
queue=self.queue, title='Test ticket')
test_user = User.objects.create_user(username='test') test_user = User.objects.create_user(username="test")
merge_ticket = Ticket.objects.create( merge_ticket = Ticket.objects.create(queue=self.queue, title="merge ticket")
queue=self.queue, title='merge ticket')
self.client.force_authenticate(staff_user) self.client.force_authenticate(staff_user)
response = self.client.put( response = self.client.put(
'/api/tickets/%d/' % test_ticket.id, "/api/tickets/%d/" % test_ticket.id,
{ {
'queue': self.queue.id, "queue": self.queue.id,
'title': 'Title', "title": "Title",
'description': 'Description', "description": "Description",
'resolution': 'Resolution', "resolution": "Resolution",
'assigned_to': test_user.id, "assigned_to": test_user.id,
'submitter_email': 'test@mail.com', "submitter_email": "test@mail.com",
'status': Ticket.RESOLVED_STATUS, "status": Ticket.RESOLVED_STATUS,
'priority': 1, "priority": 1,
'on_hold': True, "on_hold": True,
'due_date': self.due_date, "due_date": self.due_date,
'merged_to': merge_ticket.id "merged_to": merge_ticket.id,
} },
) )
self.assertEqual(response.status_code, HTTP_200_OK) self.assertEqual(response.status_code, HTTP_200_OK)
test_ticket.refresh_from_db() test_ticket.refresh_from_db()
self.assertEqual(test_ticket.title, 'Title') self.assertEqual(test_ticket.title, "Title")
self.assertEqual(test_ticket.description, 'Description') self.assertEqual(test_ticket.description, "Description")
self.assertEqual(test_ticket.resolution, 'Resolution') self.assertEqual(test_ticket.resolution, "Resolution")
self.assertEqual(test_ticket.assigned_to, test_user) 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.assertEqual(test_ticket.priority, 1)
self.assertTrue(test_ticket.on_hold) self.assertTrue(test_ticket.on_hold)
self.assertEqual(test_ticket.status, Ticket.RESOLVED_STATUS) self.assertEqual(test_ticket.status, Ticket.RESOLVED_STATUS)
@ -174,236 +189,264 @@ class TicketTest(APITestCase):
self.assertEqual(test_ticket.merged_to, merge_ticket) self.assertEqual(test_ticket.merged_to, merge_ticket)
def test_partial_edit_api_ticket(self): def test_partial_edit_api_ticket(self):
staff_user = User.objects.create_user(username='admin', is_staff=True) staff_user = User.objects.create_user(username="admin", is_staff=True)
test_ticket = Ticket.objects.create( test_ticket = Ticket.objects.create(queue=self.queue, title="Test ticket")
queue=self.queue, title='Test ticket')
self.client.force_authenticate(staff_user) self.client.force_authenticate(staff_user)
response = self.client.patch( 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) self.assertEqual(response.status_code, HTTP_200_OK)
test_ticket.refresh_from_db() 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): def test_delete_api_ticket(self):
staff_user = User.objects.create_user(username='admin', is_staff=True) staff_user = User.objects.create_user(username="admin", is_staff=True)
test_ticket = Ticket.objects.create( test_ticket = Ticket.objects.create(queue=self.queue, title="Test ticket")
queue=self.queue, title='Test ticket')
self.client.force_authenticate(staff_user) 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.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertFalse(Ticket.objects.exists()) 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): def test_create_api_ticket_with_custom_fields(self):
# Create custom fields # Create custom fields
for field_type, field_display in CustomField.DATA_TYPE_CHOICES: for field_type, field_display in CustomField.DATA_TYPE_CHOICES:
extra_data = {} extra_data = {}
if field_type in ('varchar', 'text'): if field_type in ("varchar", "text"):
extra_data['max_length'] = 10 extra_data["max_length"] = 10
if field_type == 'integer': if field_type == "integer":
# Set one field as required to test error if not provided # Set one field as required to test error if not provided
extra_data['required'] = True extra_data["required"] = True
if field_type == 'decimal': if field_type == "decimal":
extra_data['max_length'] = 7 extra_data["max_length"] = 7
extra_data['decimal_places'] = 3 extra_data["decimal_places"] = 3
if field_type == 'list': if field_type == "list":
extra_data['list_values'] = '''Green extra_data["list_values"] = """Green
Blue Blue
Red Red
Yellow''' Yellow"""
CustomField.objects.create( 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) self.client.force_authenticate(staff_user)
# Test creation without providing required field # Test creation without providing required field
response = self.client.post('/api/tickets/', { response = self.client.post(
'queue': self.queue.id, "/api/tickets/",
'title': 'Test title', {
'description': 'Test description\nMulti lines', "queue": self.queue.id,
'submitter_email': 'test@mail.com', "title": "Test title",
'priority': 4 "description": "Test description\nMulti lines",
}) "submitter_email": "test@mail.com",
"priority": 4,
},
)
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'custom_integer': [ErrorDetail( self.assertEqual(
string='This field is required.', code='required')]}) response.data,
{
"custom_integer": [
ErrorDetail(string="This field is required.", code="required")
]
},
)
# Test creation with custom field values # Test creation with custom field values
response = self.client.post('/api/tickets/', { response = self.client.post(
'queue': self.queue.id, "/api/tickets/",
'title': 'Test title', {
'description': 'Test description\nMulti lines', "queue": self.queue.id,
'submitter_email': 'test@mail.com', "title": "Test title",
'priority': 4, "description": "Test description\nMulti lines",
'custom_varchar': 'test', "submitter_email": "test@mail.com",
'custom_text': 'multi\nline', "priority": 4,
'custom_integer': '1', "custom_varchar": "test",
'custom_decimal': '42.987', "custom_text": "multi\nline",
'custom_list': 'Red', "custom_integer": "1",
'custom_boolean': True, "custom_decimal": "42.987",
'custom_date': '2022-4-11', "custom_list": "Red",
'custom_time': '23:59:59', "custom_boolean": True,
'custom_datetime': '2022-4-10 18:27', "custom_date": "2022-4-11",
'custom_email': 'email@test.com', "custom_time": "23:59:59",
'custom_url': 'http://django-helpdesk.readthedocs.org/', "custom_datetime": "2022-4-10 18:27",
'custom_ipaddress': '127.0.0.1', "custom_email": "email@test.com",
'custom_slug': 'test-slug', "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) self.assertEqual(response.status_code, HTTP_201_CREATED)
# Check all fields with data returned from the response # Check all fields with data returned from the response
self.assertEqual(response.data, { self.assertEqual(
'id': 1, response.data,
'queue': 1, {
'title': 'Test title', "id": 1,
'description': 'Test description\nMulti lines', "queue": 1,
'resolution': None, "title": "Test title",
'submitter_email': 'test@mail.com', "description": "Test description\nMulti lines",
'assigned_to': None, "resolution": None,
'status': 1, "submitter_email": "test@mail.com",
'on_hold': False, "assigned_to": None,
'priority': 4, "status": 1,
'due_date': None, "on_hold": False,
'merged_to': None, "priority": 4,
'followup_set': [OrderedDict([ "due_date": None,
('id', 1), "merged_to": None,
('ticket', 1), "followup_set": [
('user', 1), OrderedDict(
('title', 'Ticket Opened'), [
('comment', 'Test description\nMulti lines'), ("id", 1),
('public', True), ("ticket", 1),
('new_status', None), ("user", 1),
('time_spent', None), ("title", "Ticket Opened"),
('followupattachment_set', []), ("comment", "Test description\nMulti lines"),
('date', '2022-06-30T23:09:44'), ("public", True),
('message_id', None), ("new_status", None),
])], ("time_spent", None),
'custom_varchar': 'test', ("followupattachment_set", []),
'custom_text': 'multi\nline', ("date", "2022-06-30T23:09:44"),
'custom_integer': 1, ("message_id", None),
'custom_decimal': '42.987', ]
'custom_list': 'Red', )
'custom_boolean': True, ],
'custom_date': '2022-04-11', "custom_varchar": "test",
'custom_time': '23:59:59', "custom_text": "multi\nline",
'custom_datetime': '2022-04-10T18:27', "custom_integer": 1,
'custom_email': 'email@test.com', "custom_decimal": "42.987",
'custom_url': 'http://django-helpdesk.readthedocs.org/', "custom_list": "Red",
'custom_ipaddress': '127.0.0.1', "custom_boolean": True,
'custom_slug': 'test-slug' "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): 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) self.client.force_authenticate(staff_user)
test_file = SimpleUploadedFile( test_file = SimpleUploadedFile(
'file.jpg', b'file_content', content_type='image/jpg') "file.jpg", b"file_content", content_type="image/jpg"
response = self.client.post('/api/tickets/', { )
'queue': self.queue.id, response = self.client.post(
'title': 'Test title', "/api/tickets/",
'description': 'Test description\nMulti lines', {
'submitter_email': 'test@mail.com', "queue": self.queue.id,
'priority': 4, "title": "Test title",
'attachment': test_file "description": "Test description\nMulti lines",
}) "submitter_email": "test@mail.com",
"priority": 4,
"attachment": test_file,
},
)
self.assertEqual(response.status_code, HTTP_201_CREATED) self.assertEqual(response.status_code, HTTP_201_CREATED)
created_ticket = Ticket.objects.get() created_ticket = Ticket.objects.get()
self.assertEqual(created_ticket.title, 'Test title') self.assertEqual(created_ticket.title, "Test title")
self.assertEqual(created_ticket.description, self.assertEqual(created_ticket.description, "Test description\nMulti lines")
'Test description\nMulti lines') self.assertEqual(created_ticket.submitter_email, "test@mail.com")
self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
self.assertEqual(created_ticket.priority, 4) self.assertEqual(created_ticket.priority, 4)
self.assertEqual(created_ticket.followup_set.count(), 1) self.assertEqual(created_ticket.followup_set.count(), 1)
self.assertEqual(created_ticket.followup_set.get( self.assertEqual(
).followupattachment_set.count(), 1) created_ticket.followup_set.get().followupattachment_set.count(), 1
)
attachment = created_ticket.followup_set.get().followupattachment_set.get() attachment = created_ticket.followup_set.get().followupattachment_set.get()
self.assertEqual( self.assertEqual(
attachment.file.name, 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): 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) 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( 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( 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/', { response = self.client.post(
'ticket': ticket.id, "/api/followups/",
'title': 'Test', {
'comment': 'Test answer\nMulti lines', "ticket": ticket.id,
'attachments': [ "title": "Test",
test_file_1, "comment": "Test answer\nMulti lines",
test_file_2 "attachments": [test_file_1, test_file_2],
] },
}) )
self.assertEqual(response.status_code, HTTP_201_CREATED) self.assertEqual(response.status_code, HTTP_201_CREATED)
created_followup = ticket.followup_set.last() created_followup = ticket.followup_set.last()
self.assertEqual(created_followup.title, 'Test') self.assertEqual(created_followup.title, "Test")
self.assertEqual(created_followup.comment, 'Test answer\nMulti lines') self.assertEqual(created_followup.comment, "Test answer\nMulti lines")
self.assertEqual(created_followup.followupattachment_set.count(), 2) self.assertEqual(created_followup.followupattachment_set.count(), 2)
self.assertEqual( self.assertEqual(
created_followup.followupattachment_set.first().filename, 'doc.pdf') created_followup.followupattachment_set.first().filename, "doc.pdf"
)
self.assertEqual( self.assertEqual(
created_followup.followupattachment_set.first().mime_type, 'application/pdf') created_followup.followupattachment_set.first().mime_type, "application/pdf"
)
self.assertEqual( self.assertEqual(
created_followup.followupattachment_set.last().filename, 'file.jpg') created_followup.followupattachment_set.last().filename, "file.jpg"
)
self.assertEqual( self.assertEqual(
created_followup.followupattachment_set.last().mime_type, 'image/jpg') created_followup.followupattachment_set.last().mime_type, "image/jpg"
)
class UserTicketTest(APITestCase): class UserTicketTest(APITestCase):
def setUp(self): def setUp(self):
self.queue = Queue.objects.create(title='Test queue') self.queue = Queue.objects.create(title="Test queue")
self.user = User.objects.create_user(username='test') self.user = User.objects.create_user(username="test")
self.client.force_authenticate(self.user) self.client.force_authenticate(self.user)
def test_get_user_tickets(self): 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( ticket_1 = Ticket.objects.create(
queue=self.queue, title='Test 1', queue=self.queue, title="Test 1", submitter_email="foo@example.com"
submitter_email="foo@example.com") )
ticket_2 = Ticket.objects.create( ticket_2 = Ticket.objects.create(
queue=self.queue, title='Test 2', queue=self.queue, title="Test 2", submitter_email="bar@example.com"
submitter_email="bar@example.com") )
ticket_3 = Ticket.objects.create( ticket_3 = Ticket.objects.create(
queue=self.queue, title='Test 3', queue=self.queue, title="Test 3", submitter_email="foo@example.com"
submitter_email="foo@example.com") )
self.client.force_authenticate(user) 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(response.status_code, HTTP_200_OK)
self.assertEqual(len(response.data["results"]), 2) self.assertEqual(len(response.data["results"]), 2)
self.assertEqual(response.data["results"][0]['id'], ticket_3.id) 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"][1]["id"], ticket_1.id)
def test_staff_user(self): 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( ticket_1 = Ticket.objects.create(
queue=self.queue, title='Test 1', queue=self.queue, title="Test 1", submitter_email="staff@example.com"
submitter_email="staff@example.com") )
ticket_2 = Ticket.objects.create( ticket_2 = Ticket.objects.create(
queue=self.queue, title='Test 2', queue=self.queue, title="Test 2", submitter_email="foo@example.com"
submitter_email="foo@example.com") )
self.client.force_authenticate(staff_user) 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(response.status_code, HTTP_200_OK)
self.assertEqual(len(response.data["results"]), 1) self.assertEqual(len(response.data["results"]), 1)
def test_not_logged_in_user(self): def test_not_logged_in_user(self):
ticket_1 = Ticket.objects.create( ticket_1 = Ticket.objects.create(
queue=self.queue, title='Test 1', queue=self.queue, title="Test 1", submitter_email="ex@example.com"
submitter_email="ex@example.com") )
self.client.logout() 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) self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)

View File

@ -12,162 +12,161 @@ from unittest import mock
from unittest.case import skip 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) @override_settings(MEDIA_ROOT=MEDIA_DIR)
class AttachmentIntegrationTests(TestCase): class AttachmentIntegrationTests(TestCase):
fixtures = ["emailtemplate.json"]
fixtures = ['emailtemplate.json']
def setUp(self): def setUp(self):
self.queue_public = models.Queue.objects.create( self.queue_public = models.Queue.objects.create(
title='Public Queue', title="Public Queue",
slug='pub_q', slug="pub_q",
allow_public_submission=True, allow_public_submission=True,
new_ticket_cc='new.public@example.com', new_ticket_cc="new.public@example.com",
updated_ticket_cc='update.public@example.com', updated_ticket_cc="update.public@example.com",
) )
self.queue_private = models.Queue.objects.create( self.queue_private = models.Queue.objects.create(
title='Private Queue', title="Private Queue",
slug='priv_q', slug="priv_q",
allow_public_submission=False, allow_public_submission=False,
new_ticket_cc='new.private@example.com', new_ticket_cc="new.private@example.com",
updated_ticket_cc='update.private@example.com', updated_ticket_cc="update.private@example.com",
) )
self.ticket_data = { self.ticket_data = {
'title': 'Test Ticket Title', "title": "Test Ticket Title",
'body': 'Test Ticket Desc', "body": "Test Ticket Desc",
'priority': 3, "priority": 3,
'submitter_email': 'submitter@example.com', "submitter_email": "submitter@example.com",
} }
def test_create_pub_ticket_with_attachment(self): def test_create_pub_ticket_with_attachment(self):
test_file = SimpleUploadedFile( 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 = self.ticket_data.copy()
post_data.update({ post_data.update(
'queue': self.queue_public.id, {
'attachment': test_file, "queue": self.queue_public.id,
}) "attachment": test_file,
}
)
# Ensure ticket form submits with attachment successfully # Ensure ticket form submits with attachment successfully
response = self.client.post( response = self.client.post(reverse("helpdesk:home"), post_data, follow=True)
reverse('helpdesk:home'), post_data, follow=True)
self.assertContains(response, test_file.name) self.assertContains(response, test_file.name)
# Ensure attachment is available with correct content # Ensure attachment is available with correct content
att = models.FollowUpAttachment.objects.get( 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: with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk:
disk_content = file_on_disk.read() 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): def test_create_pub_ticket_with_attachment_utf8(self):
test_file = SimpleUploadedFile( test_file = SimpleUploadedFile("ß°äöü.txt", "โจ".encode("utf-8"), "text/utf-8")
'ß°äöü.txt', 'โจ'.encode('utf-8'), 'text/utf-8')
post_data = self.ticket_data.copy() post_data = self.ticket_data.copy()
post_data.update({ post_data.update(
'queue': self.queue_public.id, {
'attachment': test_file, "queue": self.queue_public.id,
}) "attachment": test_file,
}
)
# Ensure ticket form submits with attachment successfully # Ensure ticket form submits with attachment successfully
response = self.client.post( response = self.client.post(reverse("helpdesk:home"), post_data, follow=True)
reverse('helpdesk:home'), post_data, follow=True)
self.assertContains(response, test_file.name) self.assertContains(response, test_file.name)
# Ensure attachment is available with correct content # Ensure attachment is available with correct content
att = models.FollowUpAttachment.objects.get( 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), encoding="utf-8") as file_on_disk: )
disk_content = smart_str(file_on_disk.read(), 'utf-8') with open(
self.assertEqual(disk_content, 'โจ') 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.FollowUp, "save", autospec=True)
@mock.patch.object(models.FollowUpAttachment, 'save', autospec=True) @mock.patch.object(models.FollowUpAttachment, "save", autospec=True)
@mock.patch.object(models.Ticket, 'save', autospec=True) @mock.patch.object(models.Ticket, "save", autospec=True)
@mock.patch.object(models.Queue, 'save', autospec=True) @mock.patch.object(models.Queue, "save", autospec=True)
class AttachmentUnitTests(TestCase): class AttachmentUnitTests(TestCase):
def setUp(self): def setUp(self):
self.file_attrs = { self.file_attrs = {
'filename': '°ßäöü.txt', "filename": "°ßäöü.txt",
'content': 'โจ'.encode('utf-8'), "content": "โจ".encode("utf-8"),
'content-type': 'text/utf8', "content-type": "text/utf8",
} }
self.test_file = SimpleUploadedFile.from_dict(self.file_attrs) self.test_file = SimpleUploadedFile.from_dict(self.file_attrs)
self.follow_up = models.FollowUp.objects.create( self.follow_up = models.FollowUp.objects.create(
ticket=models.Ticket.objects.create( ticket=models.Ticket.objects.create(queue=models.Queue.objects.create())
queue=models.Queue.objects.create()
)
) )
@skip("Rework with model relocation") @skip("Rework with model relocation")
def test_unicode_attachment_filename(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save): def test_unicode_attachment_filename(
""" check utf-8 data is parsed correctly """ self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save
filename, fileobj = lib.process_attachments( ):
self.follow_up, [self.test_file])[0] """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( mock_att_save.assert_called_with(
file=self.test_file, file=self.test_file,
filename=self.file_attrs['filename'], filename=self.file_attrs["filename"],
mime_type=self.file_attrs['content-type'], mime_type=self.file_attrs["content-type"],
size=len(self.file_attrs['content']), 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(
followup=self.follow_up, 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() obj.save()
self.assertEqual(obj.file.name, self.file_attrs['filename']) self.assertEqual(obj.file.name, self.file_attrs["filename"])
self.assertEqual(obj.file.size, len(self.file_attrs['content'])) self.assertEqual(obj.file.size, len(self.file_attrs["content"]))
self.assertEqual(obj.file.file.content_type, "text/utf8") 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): def test_kbi_attachment(
""" check utf-8 data is parsed correctly """ 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( kbcategory = models.KBCategory.objects.create(
title="Title", title="Title", slug="slug", description="Description"
slug="slug",
description="Description"
) )
kbitem = models.KBItem.objects.create( kbitem = models.KBItem.objects.create(
category=kbcategory, category=kbcategory, title="Title", question="Question", answer="Answer"
title="Title",
question="Question",
answer="Answer"
) )
obj = models.KBIAttachment.objects.create( obj = models.KBIAttachment.objects.create(kbitem=kbitem, file=self.test_file)
kbitem=kbitem,
file=self.test_file
)
obj.save() obj.save()
self.assertEqual(obj.filename, self.file_attrs['filename']) self.assertEqual(obj.filename, self.file_attrs["filename"])
self.assertEqual(obj.file.size, len(self.file_attrs['content'])) self.assertEqual(obj.file.size, len(self.file_attrs["content"]))
self.assertEqual(obj.mime_type, "text/plain") self.assertEqual(obj.mime_type, "text/plain")
@skip("model in lib not patched") @skip("model in lib not patched")
@override_settings(MEDIA_ROOT=MEDIA_DIR) @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): def test_unicode_filename_to_filesystem(
""" don't mock saving to filesystem to test file renames caused by storage layer """ self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save
filename, fileobj = lib.process_attachments( ):
self.follow_up, [self.test_file])[0] """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 # Attachment object was zeroth positional arg (i.e. self) of att.save
# call # call
attachment_obj = mock_att_save.return_value attachment_obj = mock_att_save.return_value
mock_att_save.assert_called_once_with(attachment_obj) mock_att_save.assert_called_once_with(attachment_obj)
self.assertIsInstance(attachment_obj, models.FollowUpAttachment) 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(): def tearDownModule():

View File

@ -8,245 +8,254 @@ from helpdesk.models import Checklist, ChecklistTask, ChecklistTemplate, Queue,
class TicketChecklistTestCase(TestCase): class TicketChecklistTestCase(TestCase):
@classmethod @classmethod
def setUpTestData(cls): 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.is_staff = True
user.save() user.save()
cls.user = user cls.user = user
def setUp(self) -> None: 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): def test_create_checklist(self):
self.assertEqual(self.ticket.checklists.count(), 0) self.assertEqual(self.ticket.checklists.count(), 0)
checklist_name = 'test empty checklist' checklist_name = "test empty checklist"
response = self.client.post( response = self.client.post(
reverse('helpdesk:view', kwargs={'ticket_id': self.ticket.id}), reverse("helpdesk:view", kwargs={"ticket_id": self.ticket.id}),
data={'name': checklist_name}, data={"name": checklist_name},
follow=True follow=True,
) )
self.assertEqual(response.status_code, 200) 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.assertContains(response, checklist_name)
self.assertEqual(self.ticket.checklists.count(), 1) self.assertEqual(self.ticket.checklists.count(), 1)
def test_create_checklist_from_template(self): def test_create_checklist_from_template(self):
self.assertEqual(self.ticket.checklists.count(), 0) self.assertEqual(self.ticket.checklists.count(), 0)
checklist_name = 'test checklist from template' checklist_name = "test checklist from template"
checklist_template = ChecklistTemplate.objects.create( checklist_template = ChecklistTemplate.objects.create(
name='Test template', name="Test template", task_list=["first", "second", "last"]
task_list=['first', 'second', 'last']
) )
response = self.client.post( response = self.client.post(
reverse('helpdesk:view', kwargs={'ticket_id': self.ticket.id}), reverse("helpdesk:view", kwargs={"ticket_id": self.ticket.id}),
data={'name': checklist_name, 'checklist_template': checklist_template.id}, data={"name": checklist_name, "checklist_template": checklist_template.id},
follow=True follow=True,
) )
self.assertEqual(response.status_code, 200) 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.assertContains(response, checklist_name)
self.assertEqual(self.ticket.checklists.count(), 1) self.assertEqual(self.ticket.checklists.count(), 1)
created_checklist = self.ticket.checklists.get() created_checklist = self.ticket.checklists.get()
self.assertEqual(created_checklist.tasks.count(), 3) self.assertEqual(created_checklist.tasks.count(), 3)
self.assertEqual(created_checklist.tasks.all()[0].description, 'first') self.assertEqual(created_checklist.tasks.all()[0].description, "first")
self.assertEqual(created_checklist.tasks.all()[1].description, 'second') self.assertEqual(created_checklist.tasks.all()[1].description, "second")
self.assertEqual(created_checklist.tasks.all()[2].description, 'last') self.assertEqual(created_checklist.tasks.all()[2].description, "last")
def test_edit_checklist(self): def test_edit_checklist(self):
checklist = self.ticket.checklists.create(name='Test checklist') checklist = self.ticket.checklists.create(name="Test checklist")
first_task = checklist.tasks.create(description='First task', position=1) first_task = checklist.tasks.create(description="First task", position=1)
checklist.tasks.create(description='To delete task', position=2) checklist.tasks.create(description="To delete task", position=2)
url = reverse('helpdesk:edit_ticket_checklist', kwargs={ url = reverse(
'ticket_id': self.ticket.id, "helpdesk:edit_ticket_checklist",
'checklist_id': checklist.id, kwargs={
}) "ticket_id": self.ticket.id,
"checklist_id": checklist.id,
},
)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'helpdesk/checklist_form.html') self.assertTemplateUsed(response, "helpdesk/checklist_form.html")
self.assertContains(response, 'Test checklist') self.assertContains(response, "Test checklist")
self.assertContains(response, 'First task') self.assertContains(response, "First task")
self.assertContains(response, 'To delete task') self.assertContains(response, "To delete task")
response = self.client.post( response = self.client.post(
url, url,
data={ data={
'name': 'New name', "name": "New name",
'tasks-TOTAL_FORMS': 3, "tasks-TOTAL_FORMS": 3,
'tasks-INITIAL_FORMS': 2, "tasks-INITIAL_FORMS": 2,
'tasks-0-id': '1', "tasks-0-id": "1",
'tasks-0-description': 'First task edited', "tasks-0-description": "First task edited",
'tasks-0-position': '2', "tasks-0-position": "2",
'tasks-1-id': '2', "tasks-1-id": "2",
'tasks-1-description': 'To delete task', "tasks-1-description": "To delete task",
'tasks-1-DELETE': 'on', "tasks-1-DELETE": "on",
'tasks-1-position': '2', "tasks-1-position": "2",
'tasks-2-description': 'New first task', "tasks-2-description": "New first task",
'tasks-2-position': '1', "tasks-2-position": "1",
}, },
follow=True follow=True,
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'helpdesk/ticket.html') self.assertTemplateUsed(response, "helpdesk/ticket.html")
checklist.refresh_from_db() checklist.refresh_from_db()
self.assertEqual(checklist.name, 'New name') self.assertEqual(checklist.name, "New name")
self.assertEqual(checklist.tasks.count(), 2) self.assertEqual(checklist.tasks.count(), 2)
first_task.refresh_from_db() first_task.refresh_from_db()
self.assertEqual(first_task.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()[0].description, "New first task")
self.assertEqual(checklist.tasks.all()[1].description, 'First task edited') self.assertEqual(checklist.tasks.all()[1].description, "First task edited")
def test_delete_checklist(self): def test_delete_checklist(self):
checklist = self.ticket.checklists.create(name='Test checklist') checklist = self.ticket.checklists.create(name="Test checklist")
checklist.tasks.create(description='First task', position=1) checklist.tasks.create(description="First task", position=1)
self.assertEqual(Checklist.objects.count(), 1) self.assertEqual(Checklist.objects.count(), 1)
self.assertEqual(ChecklistTask.objects.count(), 1) self.assertEqual(ChecklistTask.objects.count(), 1)
response = self.client.post( response = self.client.post(
reverse( reverse(
'helpdesk:delete_ticket_checklist', "helpdesk:delete_ticket_checklist",
kwargs={'ticket_id': self.ticket.id, 'checklist_id': checklist.id} kwargs={"ticket_id": self.ticket.id, "checklist_id": checklist.id},
), ),
follow=True follow=True,
) )
self.assertEqual(response.status_code, 200) 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(Checklist.objects.count(), 0)
self.assertEqual(ChecklistTask.objects.count(), 0) self.assertEqual(ChecklistTask.objects.count(), 0)
def test_mark_task_as_done(self): def test_mark_task_as_done(self):
checklist = self.ticket.checklists.create(name='Test checklist') checklist = self.ticket.checklists.create(name="Test checklist")
task = checklist.tasks.create(description='Task', position=1) task = checklist.tasks.create(description="Task", position=1)
self.assertIsNone(task.completion_date) self.assertIsNone(task.completion_date)
self.assertEqual(self.ticket.followup_set.count(), 0) self.assertEqual(self.ticket.followup_set.count(), 0)
response = self.client.post( response = self.client.post(
reverse('helpdesk:update', kwargs={'ticket_id': self.ticket.id}), reverse("helpdesk:update", kwargs={"ticket_id": self.ticket.id}),
data={ data={f"checklist-{checklist.id}": task.id},
f'checklist-{checklist.id}': task.id follow=True,
},
follow=True
) )
self.assertEqual(response.status_code, 200) 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) self.assertEqual(self.ticket.followup_set.count(), 1)
followup = self.ticket.followup_set.get() followup = self.ticket.followup_set.get()
self.assertEqual(followup.ticketchange_set.count(), 1) self.assertEqual(followup.ticketchange_set.count(), 1)
self.assertEqual(followup.ticketchange_set.get().old_value, 'To do') 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().new_value, "Completed")
task.refresh_from_db() task.refresh_from_db()
self.assertIsNotNone(task.completion_date) self.assertIsNotNone(task.completion_date)
def test_mark_task_as_undone(self): def test_mark_task_as_undone(self):
checklist = self.ticket.checklists.create(name='Test checklist') checklist = self.ticket.checklists.create(name="Test checklist")
task = checklist.tasks.create(description='Task', position=1, completion_date=datetime(2023, 5, 1)) task = checklist.tasks.create(
description="Task", position=1, completion_date=datetime(2023, 5, 1)
)
self.assertIsNotNone(task.completion_date) self.assertIsNotNone(task.completion_date)
self.assertEqual(self.ticket.followup_set.count(), 0) self.assertEqual(self.ticket.followup_set.count(), 0)
response = self.client.post( response = self.client.post(
reverse('helpdesk:update', kwargs={'ticket_id': self.ticket.id}), reverse("helpdesk:update", kwargs={"ticket_id": self.ticket.id}),
follow=True follow=True,
) )
self.assertEqual(response.status_code, 200) 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) self.assertEqual(self.ticket.followup_set.count(), 1)
followup = self.ticket.followup_set.get() followup = self.ticket.followup_set.get()
self.assertEqual(followup.ticketchange_set.count(), 1) self.assertEqual(followup.ticketchange_set.count(), 1)
self.assertEqual(followup.ticketchange_set.get().old_value, 'Completed') 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().new_value, "To do")
task.refresh_from_db() task.refresh_from_db()
self.assertIsNone(task.completion_date) self.assertIsNone(task.completion_date)
def test_display_checklist_templates(self): def test_display_checklist_templates(self):
ChecklistTemplate.objects.create( ChecklistTemplate.objects.create(
name='Test checklist template', name="Test checklist template", task_list=["first", "second", "third"]
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.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html') self.assertTemplateUsed(response, "helpdesk/checklist_templates.html")
self.assertContains(response, 'Test checklist template') self.assertContains(response, "Test checklist template")
self.assertContains(response, '3 tasks') self.assertContains(response, "3 tasks")
def test_create_checklist_template(self): def test_create_checklist_template(self):
self.assertEqual(ChecklistTemplate.objects.count(), 0) self.assertEqual(ChecklistTemplate.objects.count(), 0)
response = self.client.post( response = self.client.post(
reverse('helpdesk:checklist_templates'), reverse("helpdesk:checklist_templates"),
data={ data={
'name': 'Test checklist template', "name": "Test checklist template",
'task_list': '["first", "second", "third"]' "task_list": '["first", "second", "third"]',
}, },
follow=True follow=True,
) )
self.assertEqual(response.status_code, 200) 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) self.assertEqual(ChecklistTemplate.objects.count(), 1)
checklist_template = ChecklistTemplate.objects.get() checklist_template = ChecklistTemplate.objects.get()
self.assertEqual(checklist_template.name, 'Test checklist template') self.assertEqual(checklist_template.name, "Test checklist template")
self.assertEqual(checklist_template.task_list, ['first', 'second', 'third']) self.assertEqual(checklist_template.task_list, ["first", "second", "third"])
def test_edit_checklist_template(self): def test_edit_checklist_template(self):
checklist_template = ChecklistTemplate.objects.create( checklist_template = ChecklistTemplate.objects.create(
name='Test checklist template', name="Test checklist template", task_list=["first", "second", "third"]
task_list=['first', 'second', 'third']
) )
self.assertEqual(ChecklistTemplate.objects.count(), 1) self.assertEqual(ChecklistTemplate.objects.count(), 1)
response = self.client.post( 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={ data={
'name': 'New checklist template', "name": "New checklist template",
'task_list': '["new first", "second", "third", "last"]' "task_list": '["new first", "second", "third", "last"]',
}, },
follow=True follow=True,
) )
self.assertEqual(response.status_code, 200) 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) self.assertEqual(ChecklistTemplate.objects.count(), 1)
checklist_template.refresh_from_db() checklist_template.refresh_from_db()
self.assertEqual(checklist_template.name, 'New checklist template') self.assertEqual(checklist_template.name, "New checklist template")
self.assertEqual(checklist_template.task_list, ['new first', 'second', 'third', 'last']) self.assertEqual(
checklist_template.task_list, ["new first", "second", "third", "last"]
)
def test_delete_checklist_template(self): def test_delete_checklist_template(self):
checklist_template = ChecklistTemplate.objects.create( checklist_template = ChecklistTemplate.objects.create(
name='Test checklist template', name="Test checklist template", task_list=["first", "second", "third"]
task_list=['first', 'second', 'third']
) )
self.assertEqual(ChecklistTemplate.objects.count(), 1) self.assertEqual(ChecklistTemplate.objects.count(), 1)
response = self.client.post( response = self.client.post(
reverse('helpdesk:delete_checklist_template', kwargs={'checklist_template_id': checklist_template.id}), reverse(
follow=True "helpdesk:delete_checklist_template",
kwargs={"checklist_template_id": checklist_template.id},
),
follow=True,
) )
self.assertEqual(response.status_code, 200) 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) self.assertEqual(ChecklistTemplate.objects.count(), 0)

File diff suppressed because it is too large Load Diff

View File

@ -37,52 +37,52 @@ class KBTests(TestCase):
self.user = get_staff_user() self.user = get_staff_user()
def test_kb_index(self): def test_kb_index(self):
response = self.client.get(reverse('helpdesk:kb_index')) response = self.client.get(reverse("helpdesk:kb_index"))
self.assertContains(response, 'This is a test category') self.assertContains(response, "This is a test category")
def test_kb_category(self): def test_kb_category(self):
response = self.client.get( response = self.client.get(reverse("helpdesk:kb_category", args=("test_cat",)))
reverse('helpdesk:kb_category', args=("test_cat", ))) self.assertContains(response, "This is a test category")
self.assertContains(response, 'This is a test category') self.assertContains(response, "KBItem 1")
self.assertContains(response, 'KBItem 1') self.assertContains(response, "KBItem 2")
self.assertContains(response, 'KBItem 2') self.assertContains(response, "Create New Ticket Queue:")
self.assertContains(response, 'Create New Ticket Queue:') self.client.login(username=self.user.get_username(), password="password")
self.client.login(username=self.user.get_username(), response = self.client.get(reverse("helpdesk:kb_category", args=("test_cat",)))
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, '<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( ticket = Ticket.objects.create(
title="Test ticket", title="Test ticket",
queue=self.queue, queue=self.queue,
kbitem=self.kbitem1, kbitem=self.kbitem1,
) )
ticket.save() ticket.save()
response = self.client.get( response = self.client.get(reverse("helpdesk:kb_category", args=("test_cat",)))
reverse('helpdesk:kb_category', args=("test_cat",))) self.assertContains(response, "1 open tickets")
self.assertContains(response, '1 open tickets')
def test_kb_vote(self): def test_kb_vote(self):
self.client.login(username=self.user.get_username(), self.client.login(username=self.user.get_username(), password="password")
password='password')
response = self.client.post( response = self.client.post(
reverse('helpdesk:kb_vote', args=(self.kbitem1.pk, "up")), params={}) reverse("helpdesk:kb_vote", args=(self.kbitem1.pk, "up")), params={}
cat_url = reverse('helpdesk:kb_category', )
args=("test_cat",)) + "?kbitem=1" cat_url = reverse("helpdesk:kb_category", args=("test_cat",)) + "?kbitem=1"
self.assertRedirects(response, cat_url) self.assertRedirects(response, cat_url)
response = self.client.get(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( 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) self.assertRedirects(response, cat_url)
response = self.client.get(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): def test_kb_category_iframe(self):
cat_url = reverse('helpdesk:kb_category', args=( cat_url = (
"test_cat",)) + "?kbitem=1&submitter_email=foo@bar.cz&title=lol&" reverse("helpdesk:kb_category", args=("test_cat",))
+ "?kbitem=1&submitter_email=foo@bar.cz&title=lol&"
)
response = self.client.get(cat_url) response = self.client.get(cat_url)
# Assert that query params are passed on to ticket submit form # Assert that query params are passed on to ticket submit form
self.assertContains( self.assertContains(
response, "'/tickets/submit/?queue=1&_readonly_fields_=queue&kbitem=1&submitter_email=foo%40bar.cz&amp;title=lol") response,
"'/tickets/submit/?queue=1&_readonly_fields_=queue&kbitem=1&submitter_email=foo%40bar.cz&amp;title=lol",
)

View File

@ -3,43 +3,42 @@ from django.urls import reverse
class TestLoginRedirect(TestCase): class TestLoginRedirect(TestCase):
@override_settings(LOGIN_URL="/custom/login/")
@override_settings(LOGIN_URL='/custom/login/')
def test_custom_login_view_with_url(self): def test_custom_login_view_with_url(self):
"""Test login redirect when LOGIN_URL is set to custom url""" """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 # 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 # the redirect url, so that the custom login can redirect the browser
# back to helpdesk after the login. # back to helpdesk after the login.
home_url = reverse('helpdesk:home') home_url = reverse("helpdesk:home")
expected = '/custom/login/?next={}'.format(home_url) expected = "/custom/login/?next={}".format(home_url)
self.assertRedirects(response, expected, fetch_redirect_response=False) 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): def test_custom_login_next_param(self):
"""Test that the next url parameter is correctly relayed to custom login""" """Test that the next url parameter is correctly relayed to custom login"""
next_param = "/redirect/back" next_param = "/redirect/back"
url = reverse('helpdesk:login') + "?next=" + next_param url = reverse("helpdesk:login") + "?next=" + next_param
response = self.client.get(url) 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) 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): def test_default_login_view(self):
"""Test that default login is used when LOGIN_URL is helpdesk:login""" """Test that default login is used when LOGIN_URL is helpdesk:login"""
response = self.client.get(reverse('helpdesk:login')) response = self.client.get(reverse("helpdesk:login"))
self.assertTemplateUsed(response, 'helpdesk/registration/login.html') self.assertTemplateUsed(response, "helpdesk/registration/login.html")
@override_settings(LOGIN_URL=None, SITE_ID=1) @override_settings(LOGIN_URL=None, SITE_ID=1)
def test_login_url_none(self): def test_login_url_none(self):
"""Test that default login is used when LOGIN_URL is None""" """Test that default login is used when LOGIN_URL is None"""
response = self.client.get(reverse('helpdesk:login')) response = self.client.get(reverse("helpdesk:login"))
self.assertTemplateUsed(response, 'helpdesk/registration/login.html') 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): def test_custom_login_view_with_name(self):
"""Test that LOGIN_URL can be a view name""" """Test that LOGIN_URL can be a view name"""
response = self.client.get(reverse('helpdesk:login')) response = self.client.get(reverse("helpdesk:login"))
home_url = reverse('helpdesk:home') home_url = reverse("helpdesk:home")
expected = reverse('admin:login') + "?next=" + home_url expected = reverse("admin:login") + "?next=" + home_url
self.assertRedirects(response, expected) self.assertRedirects(response, expected)

View File

@ -1,10 +1,10 @@
from django.test import SimpleTestCase from django.test import SimpleTestCase
from helpdesk.models import get_markdown from helpdesk.models import get_markdown
class MarkDown(SimpleTestCase): class MarkDown(SimpleTestCase):
"""Test work Markdown functional""" """Test work Markdown functional"""
def test_markdown_html_tab(self): def test_markdown_html_tab(self):
expected_value = "<p>&lt;div&gt;test&lt;div&gt;</p>" expected_value = "<p>&lt;div&gt;test&lt;div&gt;</p>"
input_value = "<div>test<div>" input_value = "<div>test<div>"
@ -12,7 +12,7 @@ class MarkDown(SimpleTestCase):
self.assertEqual(output_value, expected_value) self.assertEqual(output_value, expected_value)
def test_markdown_nl2br(self): 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>" expected_value = "<p>Line 1<br />\n Line 2</p>"
input_value = """Line 1 input_value = """Line 1
Line 2""" Line 2"""

View File

@ -27,15 +27,15 @@ class KBDisabledTestCase(TestCase):
"""Test proper rendering of navigation.html by accessing the dashboard""" """Test proper rendering of navigation.html by accessing the dashboard"""
from django.urls import NoReverseMatch from django.urls import NoReverseMatch
self.client.login(username=get_staff_user( self.client.login(username=get_staff_user().get_username(), password="password")
).get_username(), password='password') self.assertRaises(NoReverseMatch, reverse, "helpdesk:kb_index")
self.assertRaises(NoReverseMatch, reverse, 'helpdesk:kb_index')
try: try:
response = self.client.get(reverse('helpdesk:dashboard')) response = self.client.get(reverse("helpdesk:dashboard"))
except NoReverseMatch as e: except NoReverseMatch as e:
if 'helpdesk:kb_index' in e.message: if "helpdesk:kb_index" in e.message:
self.fail( 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: else:
raise raise
else: else:
@ -47,7 +47,9 @@ class StaffUserTestCaseMixin(object):
def setUp(self): def setUp(self):
self.original_setting = helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE 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() self.reload_views()
def tearDown(self): def tearDown(self):
@ -56,16 +58,16 @@ class StaffUserTestCaseMixin(object):
def reload_views(self): def reload_views(self):
try: try:
reload(sys.modules['helpdesk.decorators']) reload(sys.modules["helpdesk.decorators"])
reload(sys.modules['helpdesk.views.staff']) reload(sys.modules["helpdesk.views.staff"])
reload_urlconf() reload_urlconf()
except KeyError: except KeyError:
pass pass
def test_anonymous_user(self): def test_anonymous_user(self):
"""Access to the dashboard always requires a login""" """Access to the dashboard always requires a login"""
response = self.client.get(reverse('helpdesk:dashboard'), follow=True) response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
self.assertTemplateUsed(response, 'helpdesk/registration/login.html') self.assertTemplateUsed(response, "helpdesk/registration/login.html")
class NonStaffUsersAllowedTestCase(StaffUserTestCaseMixin, TestCase): class NonStaffUsersAllowedTestCase(StaffUserTestCaseMixin, TestCase):
@ -79,13 +81,16 @@ class NonStaffUsersAllowedTestCase(StaffUserTestCaseMixin, TestCase):
from helpdesk.decorators import is_helpdesk_staff from helpdesk.decorators import is_helpdesk_staff
user = User.objects.create_user( 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.assertTrue(is_helpdesk_staff(user))
self.client.login(username=user.username, password='gouda') self.client.login(username=user.username, password="gouda")
response = self.client.get(reverse('helpdesk:dashboard'), follow=True) response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
self.assertTemplateUsed(response, 'helpdesk/dashboard.html') self.assertTemplateUsed(response, "helpdesk/dashboard.html")
class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase): class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
@ -96,7 +101,10 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
super().setUp() super().setUp()
self.non_staff_user_password = "gouda" self.non_staff_user_password = "gouda"
self.non_staff_user = User.objects.create_user( 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): def test_staff_user_detection(self):
"""Staff and non-staff users are correctly identified""" """Staff and non-staff users are correctly identified"""
@ -111,19 +119,18 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
""" """
user = get_staff_user() user = get_staff_user()
self.client.login(username=user.username, password='password') self.client.login(username=user.username, password="password")
response = self.client.get(reverse('helpdesk:dashboard'), follow=True) response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
self.assertTemplateUsed(response, 'helpdesk/dashboard.html') self.assertTemplateUsed(response, "helpdesk/dashboard.html")
def test_non_staff_cannot_access_dashboard(self): def test_non_staff_cannot_access_dashboard(self):
"""When HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False, """When HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False,
non-staff users should not be able to access the dashboard. non-staff users should not be able to access the dashboard.
""" """
user = self.non_staff_user user = self.non_staff_user
self.client.login(username=user.username, self.client.login(username=user.username, password=self.non_staff_user_password)
password=self.non_staff_user_password) response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
response = self.client.get(reverse('helpdesk:dashboard'), follow=True) self.assertTemplateUsed(response, "helpdesk/registration/login.html")
self.assertTemplateUsed(response, 'helpdesk/registration/login.html')
def test_staff_rss(self): def test_staff_rss(self):
"""If HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False, """If HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False,
@ -131,9 +138,8 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
""" """
user = get_staff_user() user = get_staff_user()
self.client.login(username=user.username, password="password") self.client.login(username=user.username, password="password")
response = self.client.get( response = self.client.get(reverse("helpdesk:rss_unassigned"), follow=True)
reverse('helpdesk:rss_unassigned'), follow=True) self.assertContains(response, "Unassigned Open and Reopened tickets")
self.assertContains(response, 'Unassigned Open and Reopened tickets')
@override_settings(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE=False) @override_settings(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE=False)
def test_non_staff_cannot_rss(self): 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. non-staff users should not be able to access rss feeds.
""" """
user = self.non_staff_user user = self.non_staff_user
self.client.login(username=user.username, self.client.login(username=user.username, password=self.non_staff_user_password)
password=self.non_staff_user_password)
queue = Queue.objects.create( queue = Queue.objects.create(
title="Foo", title="Foo",
slug="test_queue", slug="test_queue",
) )
rss_urls = [ rss_urls = [
reverse('helpdesk:rss_user', args=[user.username]), reverse("helpdesk:rss_user", args=[user.username]),
reverse('helpdesk:rss_user_queue', args=[ reverse("helpdesk:rss_user_queue", args=[user.username, "test_queue"]),
user.username, 'test_queue']), reverse("helpdesk:rss_queue", args=["test_queue"]),
reverse('helpdesk:rss_queue', args=['test_queue']), reverse("helpdesk:rss_unassigned"),
reverse('helpdesk:rss_unassigned'), reverse("helpdesk:rss_activity"),
reverse('helpdesk:rss_activity'),
] ]
for rss_url in rss_urls: for rss_url in rss_urls:
response = self.client.get(rss_url, follow=True) response = self.client.get(rss_url, follow=True)
self.assertTemplateUsed( self.assertTemplateUsed(response, "helpdesk/registration/login.html")
response, 'helpdesk/registration/login.html')
class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase): class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase):
@staticmethod @staticmethod
def custom_staff_filter(user): def custom_staff_filter(user):
"""Arbitrary user validation function""" """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 HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = custom_staff_filter
@ -176,25 +183,29 @@ class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase):
from helpdesk.decorators import is_helpdesk_staff from helpdesk.decorators import is_helpdesk_staff
user = User.objects.create_user( 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.assertTrue(is_helpdesk_staff(user))
self.client.login(username=user.username, password='gouda') self.client.login(username=user.username, password="gouda")
response = self.client.get(reverse('helpdesk:dashboard'), follow=True) response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
self.assertTemplateUsed(response, 'helpdesk/dashboard.html') self.assertTemplateUsed(response, "helpdesk/dashboard.html")
def test_custom_staff_fail(self): def test_custom_staff_fail(self):
from helpdesk.decorators import is_helpdesk_staff from helpdesk.decorators import is_helpdesk_staff
user = User.objects.create_user( 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.assertFalse(is_helpdesk_staff(user))
self.client.login(username=user.username, password='frog') self.client.login(username=user.username, password="frog")
response = self.client.get(reverse('helpdesk:dashboard'), follow=True) response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
self.assertTemplateUsed(response, 'helpdesk/registration/login.html') self.assertTemplateUsed(response, "helpdesk/registration/login.html")
class HomePageAnonymousUserTestCase(TestCase): class HomePageAnonymousUserTestCase(TestCase):
@ -206,14 +217,14 @@ class HomePageAnonymousUserTestCase(TestCase):
def test_homepage(self): def test_homepage(self):
helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = True helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = True
response = self.client.get(reverse('helpdesk:home')) response = self.client.get(reverse("helpdesk:home"))
self.assertTemplateUsed('helpdesk/public_homepage.html') self.assertTemplateUsed("helpdesk/public_homepage.html")
def test_redirect_to_login(self): def test_redirect_to_login(self):
"""Unauthenticated users are redirected to the login page if HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT is True""" """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 helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = True
response = self.client.get(reverse('helpdesk:home')) response = self.client.get(reverse("helpdesk:home"))
self.assertRedirects(response, reverse('helpdesk:login')) self.assertRedirects(response, reverse("helpdesk:login"))
class HomePageTestCase(TestCase): class HomePageTestCase(TestCase):
@ -221,17 +232,17 @@ class HomePageTestCase(TestCase):
self.original_setting = helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE self.original_setting = helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = False helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = False
try: try:
reload(sys.modules['helpdesk.views.public']) reload(sys.modules["helpdesk.views.public"])
except KeyError: except KeyError:
pass pass
def tearDown(self): def tearDown(self):
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = self.original_setting 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): def assertUserRedirectedToView(self, user, view_name):
self.client.login(username=user.username, password='password') self.client.login(username=user.username, password="password")
response = self.client.get(reverse('helpdesk:home')) response = self.client.get(reverse("helpdesk:home"))
self.assertRedirects(response, reverse(view_name)) self.assertRedirects(response, reverse(view_name))
self.client.logout() self.client.logout()
@ -242,15 +253,16 @@ class HomePageTestCase(TestCase):
# login_view_ticketlist is False... # login_view_ticketlist is False...
user.usersettings_helpdesk.login_view_ticketlist = False user.usersettings_helpdesk.login_view_ticketlist = False
user.usersettings_helpdesk.save() user.usersettings_helpdesk.save()
self.assertUserRedirectedToView(user, 'helpdesk:dashboard') self.assertUserRedirectedToView(user, "helpdesk:dashboard")
def test_no_user_settings_redirect_to_dashboard(self): def test_no_user_settings_redirect_to_dashboard(self):
"""Authenticated users are redirected to the dashboard if user settings are missing""" """Authenticated users are redirected to the dashboard if user settings are missing"""
from helpdesk.models import UserSettings from helpdesk.models import UserSettings
user = get_staff_user() user = get_staff_user()
UserSettings.objects.filter(user=user).delete() UserSettings.objects.filter(user=user).delete()
self.assertUserRedirectedToView(user, 'helpdesk:dashboard') self.assertUserRedirectedToView(user, "helpdesk:dashboard")
def test_redirect_to_ticket_list(self): def test_redirect_to_ticket_list(self):
"""Authenticated users are redirected to the ticket list based on their user settings""" """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.login_view_ticketlist = True
user.usersettings_helpdesk.save() user.usersettings_helpdesk.save()
self.assertUserRedirectedToView(user, 'helpdesk:list') self.assertUserRedirectedToView(user, "helpdesk:list")
class ReturnToTicketTestCase(TestCase): class ReturnToTicketTestCase(TestCase):
@ -268,13 +280,16 @@ class ReturnToTicketTestCase(TestCase):
user = get_staff_user() user = get_staff_user()
ticket = create_ticket() ticket = create_ticket()
response = return_to_ticket(user, helpdesk_settings, 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): def test_non_staff_user(self):
from helpdesk.views.staff import return_to_ticket from helpdesk.views.staff import return_to_ticket
user = User.objects.create_user( 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() ticket = create_ticket()
response = return_to_ticket(user, helpdesk_settings, ticket) response = return_to_ticket(user, helpdesk_settings, ticket)
self.assertEqual(response['location'], ticket.ticket_url) self.assertEqual(response["location"], ticket.ticket_url)

View File

@ -10,7 +10,6 @@ from helpdesk.user import HelpdeskUser
class PerQueueStaffMembershipTestCase(TestCase): class PerQueueStaffMembershipTestCase(TestCase):
IDENTIFIERS = (1, 2) IDENTIFIERS = (1, 2)
def setUp(self): def setUp(self):
@ -19,31 +18,33 @@ class PerQueueStaffMembershipTestCase(TestCase):
and user_2 with access to queue_2 containing 4 tickets and user_2 with access to queue_2 containing 4 tickets
and superuser who should be able to access both queues 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 settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = True
self.client = Client() self.client = Client()
User = get_user_model() User = get_user_model()
self.superuser = User.objects.create( self.superuser = User.objects.create(
username='superuser', username="superuser",
is_staff=True, is_staff=True,
is_superuser=True, is_superuser=True,
) )
self.superuser.set_password('superuser') self.superuser.set_password("superuser")
self.superuser.save() self.superuser.save()
self.identifier_users = {} self.identifier_users = {}
for identifier in self.IDENTIFIERS: for identifier in self.IDENTIFIERS:
queue = self.__dict__['queue_%d' % identifier] = Queue.objects.create( queue = self.__dict__["queue_%d" % identifier] = Queue.objects.create(
title='Queue %d' % identifier, title="Queue %d" % identifier,
slug='q%d' % identifier, slug="q%d" % identifier,
) )
user = self.__dict__['user_%d' % identifier] = User.objects.create( user = self.__dict__["user_%d" % identifier] = User.objects.create(
username='User_%d' % identifier, username="User_%d" % identifier,
is_staff=True, is_staff=True,
email="foo%s@example.com" % identifier email="foo%s@example.com" % identifier,
) )
user.set_password(str(identifier)) user.set_password(str(identifier))
user.save() user.save()
@ -55,13 +56,13 @@ class PerQueueStaffMembershipTestCase(TestCase):
for ticket_number in range(1, identifier + 1): for ticket_number in range(1, identifier + 1):
Ticket.objects.create( Ticket.objects.create(
title='Unassigned Ticket %d in Queue %d' % ( title="Unassigned Ticket %d in Queue %d"
ticket_number, identifier), % (ticket_number, identifier),
queue=queue, queue=queue,
) )
Ticket.objects.create( Ticket.objects.create(
title='Ticket %d in Queue %d Assigned to User_%d' % ( title="Ticket %d in Queue %d Assigned to User_%d"
ticket_number, identifier, identifier), % (ticket_number, identifier, identifier),
queue=queue, queue=queue,
assigned_to=user, assigned_to=user,
) )
@ -70,7 +71,9 @@ class PerQueueStaffMembershipTestCase(TestCase):
""" """
Reset HELPDESK_ENABLE_PER_QUEUE_STAFF_MEMBERSHIP to original value 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): def test_dashboard_ticket_counts(self):
""" """
@ -81,33 +84,32 @@ class PerQueueStaffMembershipTestCase(TestCase):
# Regular users # Regular users
for identifier in self.IDENTIFIERS: for identifier in self.IDENTIFIERS:
self.client.login(username='User_%d' % self.client.login(username="User_%d" % identifier, password=str(identifier))
identifier, password=str(identifier)) response = self.client.get(reverse("helpdesk:dashboard"))
response = self.client.get(reverse('helpdesk:dashboard'))
self.assertEqual( self.assertEqual(
len(response.context['unassigned_tickets']), len(response.context["unassigned_tickets"]),
identifier, identifier,
'Unassigned tickets were not properly limited by queue membership' "Unassigned tickets were not properly limited by queue membership",
) )
self.assertEqual( self.assertEqual(
response.context['basic_ticket_stats']['open_ticket_stats'][0][1], response.context["basic_ticket_stats"]["open_ticket_stats"][0][1],
identifier * 2, identifier * 2,
'Basic ticket stats were not properly limited by queue membership' "Basic ticket stats were not properly limited by queue membership",
) )
# Superuser # Superuser
self.client.login(username='superuser', password='superuser') self.client.login(username="superuser", password="superuser")
response = self.client.get(reverse('helpdesk:dashboard')) response = self.client.get(reverse("helpdesk:dashboard"))
self.assertEqual( self.assertEqual(
len(response.context['unassigned_tickets']), len(response.context["unassigned_tickets"]),
3, 3,
'Unassigned tickets were limited by queue membership for a superuser' "Unassigned tickets were limited by queue membership for a superuser",
) )
self.assertEqual( self.assertEqual(
response.context['basic_ticket_stats']['open_ticket_stats'][0][1] + 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"][1][1],
6, 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): def test_report_ticket_counts(self):
@ -119,44 +121,43 @@ class PerQueueStaffMembershipTestCase(TestCase):
# Regular users # Regular users
for identifier in self.IDENTIFIERS: for identifier in self.IDENTIFIERS:
self.client.login(username='User_%d' % self.client.login(username="User_%d" % identifier, password=str(identifier))
identifier, password=str(identifier)) response = self.client.get(reverse("helpdesk:report_index"))
response = self.client.get(reverse('helpdesk:report_index'))
self.assertEqual( self.assertEqual(
len(response.context['dash_tickets']), len(response.context["dash_tickets"]),
1, 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( self.assertEqual(
response.context['dash_tickets'][0]['open'], response.context["dash_tickets"][0]["open"],
identifier * 2, 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( self.assertEqual(
response.context['basic_ticket_stats']['open_ticket_stats'][0][1], response.context["basic_ticket_stats"]["open_ticket_stats"][0][1],
identifier * 2, identifier * 2,
'Basic ticket stats were not properly limited by queue membership' "Basic ticket stats were not properly limited by queue membership",
) )
# Superuser # Superuser
self.client.login(username='superuser', password='superuser') self.client.login(username="superuser", password="superuser")
response = self.client.get(reverse('helpdesk:report_index')) response = self.client.get(reverse("helpdesk:report_index"))
self.assertEqual( self.assertEqual(
len(response.context['dash_tickets']), len(response.context["dash_tickets"]),
2, 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( self.assertEqual(
response.context['dash_tickets'][0]['open'] + response.context["dash_tickets"][0]["open"]
response.context['dash_tickets'][1]['open'], + response.context["dash_tickets"][1]["open"],
6, 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( self.assertEqual(
response.context['basic_ticket_stats']['open_ticket_stats'][0][1] + 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"][1][1],
6, 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): def test_ticket_list_per_queue_user_restrictions(self):
@ -167,36 +168,38 @@ class PerQueueStaffMembershipTestCase(TestCase):
""" """
# Regular users # Regular users
for identifier in self.IDENTIFIERS: for identifier in self.IDENTIFIERS:
self.client.login(username='User_%d' % self.client.login(username="User_%d" % identifier, password=str(identifier))
identifier, password=str(identifier)) response = self.client.get(reverse("helpdesk:list"))
response = self.client.get(reverse('helpdesk:list')) tickets = __Query__(
tickets = __Query__(HelpdeskUser( HelpdeskUser(self.identifier_users[identifier]),
self.identifier_users[identifier]), base64query=response.context['urlsafe_query']).get() base64query=response.context["urlsafe_query"],
).get()
self.assertEqual( self.assertEqual(
len(tickets), len(tickets),
identifier * 2, identifier * 2,
'Ticket list was not properly limited by queue membership' "Ticket list was not properly limited by queue membership",
) )
self.assertEqual( self.assertEqual(
len(response.context['queue_choices']), len(response.context["queue_choices"]),
1, 1,
'Queue choices were not properly limited by queue membership' "Queue choices were not properly limited by queue membership",
) )
self.assertEqual( self.assertEqual(
response.context['queue_choices'][0], response.context["queue_choices"][0],
Queue.objects.get(title="Queue %d" % identifier), 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 # Superuser
self.client.login(username='superuser', password='superuser') self.client.login(username="superuser", password="superuser")
response = self.client.get(reverse('helpdesk:list')) response = self.client.get(reverse("helpdesk:list"))
tickets = __Query__(HelpdeskUser(self.superuser), tickets = __Query__(
base64query=response.context['urlsafe_query']).get() HelpdeskUser(self.superuser), base64query=response.context["urlsafe_query"]
).get()
self.assertEqual( self.assertEqual(
len(tickets), len(tickets),
6, 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): def test_ticket_reports_per_queue_user_restrictions(self):
@ -207,61 +210,60 @@ class PerQueueStaffMembershipTestCase(TestCase):
""" """
# Regular users # Regular users
for identifier in self.IDENTIFIERS: for identifier in self.IDENTIFIERS:
self.client.login(username='User_%d' % self.client.login(username="User_%d" % identifier, password=str(identifier))
identifier, password=str(identifier))
response = self.client.get( 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 # Only two columns of data should be present: ticket counts for
# unassigned and this user only # unassigned and this user only
self.assertEqual( self.assertEqual(
len(response.context['data']), len(response.context["data"]),
2, 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 # Each user should see a total number of tickets equal to twice
# their ID # their ID
self.assertEqual( self.assertEqual(
sum([sum(user_tickets[1:]) sum(
for user_tickets in response.context['data']]), [sum(user_tickets[1:]) for user_tickets in response.context["data"]]
),
identifier * 2, 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 # Each user should only be able to pick 1 queue
self.assertEqual( self.assertEqual(
len(response.context['headings']), len(response.context["headings"]),
2, 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 # The queue each user can pick should be the queue named after
# their ID # their ID
self.assertEqual( self.assertEqual(
response.context['headings'][1], response.context["headings"][1],
"Queue %d" % identifier, "Queue %d" % identifier,
'Queue choices were not properly limited by queue membership' "Queue choices were not properly limited by queue membership",
) )
# Superuser # Superuser
self.client.login(username='superuser', password='superuser') self.client.login(username="superuser", password="superuser")
response = self.client.get( 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 # Superuser should see ticket counts for all two queues, which includes
# three columns: unassigned and both user 1 and user 2 # three columns: unassigned and both user 1 and user 2
self.assertEqual( self.assertEqual(
len(response.context['data'][0]), len(response.context["data"][0]),
3, 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 # Superuser should see the total ticket count of three tickets
self.assertEqual( self.assertEqual(
sum([sum(user_tickets[1:]) sum([sum(user_tickets[1:]) for user_tickets in response.context["data"]]),
for user_tickets in response.context['data']]),
6, 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( self.assertEqual(
len(response.context['headings']), len(response.context["headings"]),
3, 3,
'Queue choices were improperly limited by queue membership for a superuser' "Queue choices were improperly limited by queue membership for a superuser",
) )

View File

@ -16,39 +16,51 @@ class PublicActionsTestCase(TestCase):
""" """
Create a queue & ticket we can use for later tests. Create a queue & ticket we can use for later tests.
""" """
self.queue = Queue.objects.create(title='Queue 1', self.queue = Queue.objects.create(
slug='q', title="Queue 1",
allow_public_submission=True, slug="q",
new_ticket_cc='new.public@example.com', allow_public_submission=True,
updated_ticket_cc='update.public@example.com') new_ticket_cc="new.public@example.com",
self.ticket = Ticket.objects.create(title='Test Ticket', updated_ticket_cc="update.public@example.com",
queue=self.queue, )
submitter_email='test.submitter@example.com', self.ticket = Ticket.objects.create(
description='This is a test ticket.') title="Test Ticket",
queue=self.queue,
submitter_email="test.submitter@example.com",
description="This is a test ticket.",
)
self.client = Client() self.client = Client()
def test_public_view_ticket(self): def test_public_view_ticket(self):
# Without key, we get 403 # Without key, we get 403
response = self.client.get('%s?ticket=%s&email=%s' % ( response = self.client.get(
reverse('helpdesk:public_view'), "%s?ticket=%s&email=%s"
self.ticket.ticket_for_url, % (
'test.submitter@example.com')) reverse("helpdesk:public_view"),
self.ticket.ticket_for_url,
"test.submitter@example.com",
)
)
self.assertEqual(response.status_code, 403) 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 # With a key it works
response = self.client.get('%s?ticket=%s&email=%s&key=%s' % ( response = self.client.get(
reverse('helpdesk:public_view'), "%s?ticket=%s&email=%s&key=%s"
self.ticket.ticket_for_url, % (
'test.submitter@example.com', reverse("helpdesk:public_view"),
self.ticket.secret_key)) self.ticket.ticket_for_url,
"test.submitter@example.com",
self.ticket.secret_key,
)
)
self.assertEqual(response.status_code, 200) 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): def test_public_close(self):
old_status = self.ticket.status old_status = self.ticket.status
old_resolution = self.ticket.resolution 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) ticket = Ticket.objects.get(id=self.ticket.id)
@ -58,20 +70,23 @@ class PublicActionsTestCase(TestCase):
current_followups = ticket.followup_set.all().count() current_followups = ticket.followup_set.all().count()
response = self.client.get('%s?ticket=%s&email=%s&close&key=%s' % ( response = self.client.get(
reverse('helpdesk:public_view'), "%s?ticket=%s&email=%s&close&key=%s"
ticket.ticket_for_url, % (
'test.submitter@example.com', reverse("helpdesk:public_view"),
ticket.secret_key)) ticket.ticket_for_url,
"test.submitter@example.com",
ticket.secret_key,
)
)
ticket = Ticket.objects.get(id=self.ticket.id) ticket = Ticket.objects.get(id=self.ticket.id)
self.assertEqual(response.status_code, 302) 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.status, Ticket.CLOSED_STATUS)
self.assertEqual(ticket.resolution, resolution_text) self.assertEqual(ticket.resolution, resolution_text)
self.assertEqual(current_followups + 1, self.assertEqual(current_followups + 1, ticket.followup_set.all().count())
ticket.followup_set.all().count())
ticket.resolution = old_resolution ticket.resolution = old_resolution
ticket.status = old_status ticket.status = old_status

View File

@ -47,25 +47,57 @@ class QueryTests(TestCase):
"""Create a staff user and login""" """Create a staff user and login"""
User = get_user_model() User = get_user_model()
self.user = User.objects.create( self.user = User.objects.create(
username='User_1', username="User_1",
is_staff=is_staff, is_staff=is_staff,
) )
self.user.set_password('pass') self.user.set_password("pass")
self.user.save() self.user.save()
self.client.login(username='User_1', password='pass') self.client.login(username="User_1", password="pass")
def test_query_basic(self): def test_query_basic(self):
self.loginUser() self.loginUser()
query = query_to_base64({}) query = query_to_base64({})
response = self.client.get( response = self.client.get(
reverse('helpdesk:datatables_ticket_list', args=[query])) reverse("helpdesk:datatables_ticket_list", args=[query])
)
resp_json = response.json() resp_json = response.json()
self.assertEqual( self.assertEqual(
resp_json, resp_json,
{ {
"data": "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"}], "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, "recordsFiltered": 2,
"recordsTotal": 2, "recordsTotal": 2,
"draw": 0, "draw": 0,
@ -74,18 +106,32 @@ class QueryTests(TestCase):
def test_query_by_kbitem(self): def test_query_by_kbitem(self):
self.loginUser() self.loginUser()
query = query_to_base64( query = query_to_base64({"filtering": {"kbitem__in": [self.kbitem1.pk]}})
{'filtering': {'kbitem__in': [self.kbitem1.pk]}}
)
response = self.client.get( response = self.client.get(
reverse('helpdesk:datatables_ticket_list', args=[query])) reverse("helpdesk:datatables_ticket_list", args=[query])
)
resp_json = response.json() resp_json = response.json()
self.assertEqual( self.assertEqual(
resp_json, resp_json,
{ {
"data": "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"}], "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, "recordsFiltered": 1,
"recordsTotal": 1, "recordsTotal": 1,
"draw": 0, "draw": 0,
@ -94,18 +140,32 @@ class QueryTests(TestCase):
def test_query_by_no_kbitem(self): def test_query_by_no_kbitem(self):
self.loginUser() self.loginUser()
query = query_to_base64( query = query_to_base64({"filtering_null": {"kbitem__isnull": True}})
{'filtering_null': {'kbitem__isnull': True}}
)
response = self.client.get( response = self.client.get(
reverse('helpdesk:datatables_ticket_list', args=[query])) reverse("helpdesk:datatables_ticket_list", args=[query])
)
resp_json = response.json() resp_json = response.json()
self.assertEqual( self.assertEqual(
resp_json, resp_json,
{ {
"data": "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": "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, "recordsFiltered": 1,
"recordsTotal": 1, "recordsTotal": 1,
"draw": 0, "draw": 0,

View File

@ -7,24 +7,25 @@ from helpdesk.tests.helpers import get_user
class TestSavingSharedQuery(TestCase): class TestSavingSharedQuery(TestCase):
def setUp(self): def setUp(self):
q = Queue(title='Q1', slug='q1') q = Queue(title="Q1", slug="q1")
q.save() q.save()
self.q = q self.q = q
def test_cansavequery(self): def test_cansavequery(self):
"""Can a query be saved""" """Can a query be saved"""
url = reverse('helpdesk:savequery') url = reverse("helpdesk:savequery")
self.client.login(username=get_user(is_staff=True).get_username(), self.client.login(
password='password') username=get_user(is_staff=True).get_username(), password="password"
)
response = self.client.post( response = self.client.post(
url, url,
data={ data={
'title': 'ticket on my queue', "title": "ticket on my queue",
'queue': self.q, "queue": self.q,
'shared': 'on', "shared": "on",
'query_encoded': "query_encoded": "KGRwMApWZmlsdGVyaW5nCnAxCihkcDIKVnN0YXR1c19faW4KcDMKKG"
'KGRwMApWZmlsdGVyaW5nCnAxCihkcDIKVnN0YXR1c19faW4KcDMKKG' "xwNApJMQphSTIKYUkzCmFzc1Zzb3J0aW5nCnA1ClZjcmVhdGVkCnA2CnMu",
'xwNApJMQphSTIKYUkzCmFzc1Zzb3J0aW5nCnA1ClZjcmVhdGVkCnA2CnMu' },
}) )
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertTrue('tickets/?saved_query=1' in response.url) self.assertTrue("tickets/?saved_query=1" in response.url)

View File

@ -17,29 +17,29 @@ except ImportError: # python 2
class TicketActionsTestCase(TestCase): class TicketActionsTestCase(TestCase):
fixtures = ['emailtemplate.json'] fixtures = ["emailtemplate.json"]
def setUp(self): def setUp(self):
self.queue_public = Queue.objects.create( self.queue_public = Queue.objects.create(
title='Queue 1', title="Queue 1",
slug='q1', slug="q1",
allow_public_submission=True, allow_public_submission=True,
new_ticket_cc='new.public@example.com', new_ticket_cc="new.public@example.com",
updated_ticket_cc='update.public@example.com' updated_ticket_cc="update.public@example.com",
) )
self.queue_private = Queue.objects.create( self.queue_private = Queue.objects.create(
title='Queue 2', title="Queue 2",
slug='q2', slug="q2",
allow_public_submission=False, allow_public_submission=False,
new_ticket_cc='new.private@example.com', new_ticket_cc="new.private@example.com",
updated_ticket_cc='update.private@example.com' updated_ticket_cc="update.private@example.com",
) )
self.ticket_data = { self.ticket_data = {
'queue': self.queue_public, "queue": self.queue_public,
'title': 'Test Ticket', "title": "Test Ticket",
'description': 'Some Test Ticket', "description": "Some Test Ticket",
} }
self.client = Client() self.client = Client()
@ -49,24 +49,22 @@ class TicketActionsTestCase(TestCase):
"""Create a staff user and login""" """Create a staff user and login"""
User = get_user_model() User = get_user_model()
self.user = User.objects.create( self.user = User.objects.create(
username='User_1', username="User_1",
is_staff=is_staff, is_staff=is_staff,
) )
self.user.set_password('pass') self.user.set_password("pass")
self.user.save() self.user.save()
self.client.login(username='User_1', password='pass') self.client.login(username="User_1", password="pass")
def test_ticket_markdown(self): def test_ticket_markdown(self):
ticket_data = { ticket_data = {
'queue': self.queue_public, "queue": self.queue_public,
'title': 'Test Ticket', "title": "Test Ticket",
'description': '*bold*', "description": "*bold*",
} }
ticket = Ticket.objects.create(**ticket_data) ticket = Ticket.objects.create(**ticket_data)
self.assertEqual(ticket.get_markdown(), self.assertEqual(ticket.get_markdown(), "<p><em>bold</em></p>")
"<p><em>bold</em></p>")
def test_delete_ticket_staff(self): def test_delete_ticket_staff(self):
# make staff user # make staff user
@ -76,13 +74,14 @@ class TicketActionsTestCase(TestCase):
ticket = Ticket.objects.create(**self.ticket_data) ticket = Ticket.objects.create(**self.ticket_data)
ticket_id = ticket.id ticket_id = ticket.id
response = self.client.get(reverse('helpdesk:delete', kwargs={ response = self.client.get(
'ticket_id': ticket_id}), follow=True) reverse("helpdesk:delete", kwargs={"ticket_id": ticket_id}), follow=True
self.assertContains( )
response, 'Are you sure you want to delete this ticket') self.assertContains(response, "Are you sure you want to delete this ticket")
response = self.client.post(reverse('helpdesk:delete', kwargs={ response = self.client.post(
'ticket_id': ticket_id}), follow=True) reverse("helpdesk:delete", kwargs={"ticket_id": ticket_id}), follow=True
)
first_redirect = response.redirect_chain[0] first_redirect = response.redirect_chain[0]
first_redirect_url = first_redirect[0] first_redirect_url = first_redirect[0]
@ -90,7 +89,7 @@ class TicketActionsTestCase(TestCase):
# Django 1.9 compatible way of testing this # 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 # https://docs.djangoproject.com/en/1.9/releases/1.9/#http-redirects-no-longer-forced-to-absolute-uris
urlparts = urlparse(first_redirect_url) urlparts = urlparse(first_redirect_url)
self.assertEqual(urlparts.path, reverse('helpdesk:home')) self.assertEqual(urlparts.path, reverse("helpdesk:home"))
# test ticket deleted # test ticket deleted
with self.assertRaises(Ticket.DoesNotExist): with self.assertRaises(Ticket.DoesNotExist):
@ -105,15 +104,15 @@ class TicketActionsTestCase(TestCase):
# create second user # create second user
User = get_user_model() User = get_user_model()
self.user2 = User.objects.create( self.user2 = User.objects.create(
username='User_2', username="User_2",
is_staff=True, is_staff=True,
) )
initial_data = { initial_data = {
'title': 'Private ticket test', "title": "Private ticket test",
'queue': self.queue_public, "queue": self.queue_public,
'assigned_to': self.user, "assigned_to": self.user,
'status': Ticket.OPEN_STATUS, "status": Ticket.OPEN_STATUS,
} }
# create ticket # create ticket
@ -122,39 +121,45 @@ class TicketActionsTestCase(TestCase):
# assign new owner # assign new owner
post_data = { post_data = {
'owner': self.user2.id, "owner": self.user2.id,
} }
response = self.client.post(reverse('helpdesk:update', kwargs={ response = self.client.post(
'ticket_id': ticket_id}), post_data, follow=True) reverse("helpdesk:update", kwargs={"ticket_id": ticket_id}),
self.assertContains(response, 'Changed Owner from User_1 to User_2') 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, # change status with users email assigned and submitter email assigned,
# which triggers emails being sent # which triggers emails being sent
ticket.assigned_to = self.user2 ticket.assigned_to = self.user2
ticket.submitter_email = 'submitter@test.com' ticket.submitter_email = "submitter@test.com"
ticket.save() ticket.save()
self.user2.email = 'user2@test.com' self.user2.email = "user2@test.com"
self.user2.save() self.user2.save()
self.user.email = 'user1@test.com' self.user.email = "user1@test.com"
self.user.save() self.user.save()
post_data = { post_data = {"new_status": Ticket.CLOSED_STATUS, "public": True}
'new_status': Ticket.CLOSED_STATUS,
'public': True
}
# do this also to a newly assigned user (different from logged in one) # do this also to a newly assigned user (different from logged in one)
ticket.assigned_to = self.user ticket.assigned_to = self.user
response = self.client.post(reverse('helpdesk:update', kwargs={ response = self.client.post(
'ticket_id': ticket_id}), post_data, follow=True) reverse("helpdesk:update", kwargs={"ticket_id": ticket_id}),
self.assertContains(response, 'Changed Status from Open to Closed') post_data,
follow=True,
)
self.assertContains(response, "Changed Status from Open to Closed")
post_data = { post_data = {
'new_status': Ticket.OPEN_STATUS, "new_status": Ticket.OPEN_STATUS,
'owner': self.user2.id, "owner": self.user2.id,
'public': True "public": True,
} }
response = self.client.post(reverse('helpdesk:update', kwargs={ response = self.client.post(
'ticket_id': ticket_id}), post_data, follow=True) reverse("helpdesk:update", kwargs={"ticket_id": ticket_id}),
self.assertContains(response, 'Changed Status from Open to Closed') post_data,
follow=True,
)
self.assertContains(response, "Changed Status from Open to Closed")
def test_can_access_ticket(self): def test_can_access_ticket(self):
"""Tests whether non-staff but assigned user still counts as owner""" """Tests whether non-staff but assigned user still counts as owner"""
@ -165,24 +170,22 @@ class TicketActionsTestCase(TestCase):
# create second user # create second user
User = get_user_model() User = get_user_model()
self.user2 = User.objects.create( self.user2 = User.objects.create(
username='User_2', username="User_2",
is_staff=False, is_staff=False,
) )
initial_data = { initial_data = {
'title': 'Private ticket test', "title": "Private ticket test",
'queue': self.queue_private, "queue": self.queue_private,
'assigned_to': self.user, "assigned_to": self.user,
'status': Ticket.OPEN_STATUS, "status": Ticket.OPEN_STATUS,
} }
# create ticket # create ticket
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = True helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = True
ticket = Ticket.objects.create(**initial_data) ticket = Ticket.objects.create(**initial_data)
self.assertEqual(HelpdeskUser( self.assertEqual(HelpdeskUser(self.user).can_access_ticket(ticket), True)
self.user).can_access_ticket(ticket), True) self.assertEqual(HelpdeskUser(self.user2).can_access_ticket(ticket), False)
self.assertEqual(HelpdeskUser(
self.user2).can_access_ticket(ticket), False)
def test_num_to_link(self): def test_num_to_link(self):
"""Test that we are correctly expanding links to tickets from IDs""" """Test that we are correctly expanding links to tickets from IDs"""
@ -191,10 +194,10 @@ class TicketActionsTestCase(TestCase):
self.loginUser() self.loginUser()
initial_data = { initial_data = {
'title': 'Some private ticket', "title": "Some private ticket",
'queue': self.queue_public, "queue": self.queue_public,
'assigned_to': self.user, "assigned_to": self.user,
'status': Ticket.OPEN_STATUS, "status": Ticket.OPEN_STATUS,
} }
# create ticket # create ticket
@ -202,18 +205,23 @@ class TicketActionsTestCase(TestCase):
ticket_id = ticket.id ticket_id = ticket.id
# generate the URL text # 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( 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( result2 = num_to_link("whoa another ticket is here #%s huh" % ticket_id)
'whoa another ticket is here #%s huh' % ticket_id)
self.assertEqual( 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): def test_create_ticket_getform(self):
self.loginUser() 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) self.assertEqual(response.status_code, 200)
# TODO this needs to be checked further # TODO this needs to be checked further
@ -224,61 +232,62 @@ class TicketActionsTestCase(TestCase):
# Create two tickets # Create two tickets
ticket_1 = Ticket.objects.create( ticket_1 = Ticket.objects.create(
queue=self.queue_public, queue=self.queue_public,
title='Ticket 1', title="Ticket 1",
description='Description from ticket 1', description="Description from ticket 1",
submitter_email='user1@mail.com', submitter_email="user1@mail.com",
status=Ticket.RESOLVED_STATUS, 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( ticket_1_follow_up = ticket_1.followup_set.create(title="Ticket 1 creation")
title='Ticket 1 creation')
ticket_1_cc = ticket_1.ticketcc_set.create(user=self.user) ticket_1_cc = ticket_1.ticketcc_set.create(user=self.user)
ticket_1_created = ticket_1.created ticket_1_created = ticket_1.created
due_date = timezone.now() due_date = timezone.now()
ticket_2 = Ticket.objects.create( ticket_2 = Ticket.objects.create(
queue=self.queue_public, queue=self.queue_public,
title='Ticket 2', title="Ticket 2",
description='Description from ticket 2', description="Description from ticket 2",
submitter_email='user2@mail.com', submitter_email="user2@mail.com",
due_date=due_date, due_date=due_date,
assigned_to=self.user assigned_to=self.user,
) )
ticket_2_follow_up = ticket_1.followup_set.create( ticket_2_follow_up = ticket_1.followup_set.create(title="Ticket 2 creation")
title='Ticket 2 creation') ticket_2_cc = ticket_2.ticketcc_set.create(email="random@mail.com")
ticket_2_cc = ticket_2.ticketcc_set.create(email='random@mail.com')
# Create custom fields and set values for tickets # Create custom fields and set values for tickets
custom_field_1 = CustomField.objects.create( custom_field_1 = CustomField.objects.create(
name='test', name="test",
label='Test', label="Test",
data_type='varchar', 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( ticket_1.ticketcustomfieldvalue_set.create(
field=custom_field_1, value=ticket_1_field_1) 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',
) )
ticket_2_field_2 = '444' ticket_2_field_1 = "Another test text"
ticket_2.ticketcustomfieldvalue_set.create( 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 # Check that it correctly redirects to the intermediate page
response = self.client.post( response = self.client.post(
reverse('helpdesk:mass_update'), reverse("helpdesk:mass_update"),
data={ data={"ticket_id": [str(ticket_1.id), str(ticket_2.id)], "action": "merge"},
'ticket_id': [str(ticket_1.id), str(ticket_2.id)], follow=True,
'action': 'merge' )
}, redirect_url = "%s?tickets=%s&tickets=%s" % (
follow=True 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.assertRedirects(response, redirect_url)
self.assertContains(response, ticket_1.description) self.assertContains(response, ticket_1.description)
self.assertContains(response, ticket_1.resolution) self.assertContains(response, ticket_1.resolution)
@ -293,16 +302,16 @@ class TicketActionsTestCase(TestCase):
response = self.client.post( response = self.client.post(
redirect_url, redirect_url,
data={ data={
'chosen_ticket': str(ticket_1.id), "chosen_ticket": str(ticket_1.id),
'due_date': str(ticket_2.id), "due_date": str(ticket_2.id),
'status': str(ticket_1.id), "status": str(ticket_1.id),
'submitter_email': str(ticket_2.id), "submitter_email": str(ticket_2.id),
'description': str(ticket_2.id), "description": str(ticket_2.id),
'assigned_to': str(ticket_2.id), "assigned_to": str(ticket_2.id),
custom_field_1.name: str(ticket_1.id), custom_field_1.name: str(ticket_1.id),
custom_field_2.name: str(ticket_2.id), custom_field_2.name: str(ticket_2.id),
}, },
follow=True follow=True,
) )
self.assertRedirects(response, ticket_1.get_absolute_url()) self.assertRedirects(response, ticket_1.get_absolute_url())
ticket_2.refresh_from_db() 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.submitter_email, ticket_2.submitter_email)
self.assertEqual(ticket_1.description, ticket_2.description) self.assertEqual(ticket_1.description, ticket_2.description)
self.assertEqual(ticket_1.assigned_to, ticket_2.assigned_to) self.assertEqual(ticket_1.assigned_to, ticket_2.assigned_to)
self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get( self.assertEqual(
field=custom_field_1).value, ticket_1_field_1) ticket_1.ticketcustomfieldvalue_set.get(field=custom_field_1).value,
self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get( ticket_1_field_1,
field=custom_field_2).value, ticket_2_field_2) )
self.assertEqual(list(ticket_1.followup_set.all()), [ self.assertEqual(
ticket_1_follow_up, ticket_2_follow_up]) ticket_1.ticketcustomfieldvalue_set.get(field=custom_field_2).value,
self.assertEqual(list(ticket_1.ticketcc_set.all()), ticket_2_field_2,
[ticket_1_cc, ticket_2_cc]) )
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): def test_update_ticket_queue(self):
"""Tests whether user can change the queue in the Respond to this ticket section.""" """Tests whether user can change the queue in the Respond to this ticket section."""
@ -333,10 +346,10 @@ class TicketActionsTestCase(TestCase):
# create ticket # create ticket
initial_data = { initial_data = {
'title': 'Queue change ticket test', "title": "Queue change ticket test",
'queue': self.queue_public, "queue": self.queue_public,
'assigned_to': self.user, "assigned_to": self.user,
'status': Ticket.OPEN_STATUS, "status": Ticket.OPEN_STATUS,
} }
ticket = Ticket.objects.create(**initial_data) ticket = Ticket.objects.create(**initial_data)
ticket_id = ticket.id ticket_id = ticket.id
@ -346,24 +359,24 @@ class TicketActionsTestCase(TestCase):
# POST first follow-up with new queue # POST first follow-up with new queue
new_queue = Queue.objects.create( new_queue = Queue.objects.create(
title='New Queue', title="New Queue",
slug='newqueue', slug="newqueue",
) )
post_data = { post_data = {
'comment': 'first follow-up in new queue', "comment": "first follow-up in new queue",
'queue': str(new_queue.id), "queue": str(new_queue.id),
} }
response = self.client.post(reverse('helpdesk:update', response = self.client.post(
kwargs={'ticket_id': ticket_id}), reverse("helpdesk:update", kwargs={"ticket_id": ticket_id}), post_data
post_data) )
# queue was correctly modified # queue was correctly modified
ticket.refresh_from_db() ticket.refresh_from_db()
self.assertEqual(ticket.queue, new_queue) self.assertEqual(ticket.queue, new_queue)
# ticket change was saved # ticket change was saved
latest_fup = ticket.followup_set.latest('date') latest_fup = ticket.followup_set.latest("date")
latest_ticketchange = latest_fup.ticketchange_set.latest('id') latest_ticketchange = latest_fup.ticketchange_set.latest("id")
self.assertEqual(latest_ticketchange.field, _('Queue')) self.assertEqual(latest_ticketchange.field, _("Queue"))
self.assertEqual(int(latest_ticketchange.old_value), self.queue_public.id) 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)

View File

@ -9,14 +9,12 @@ from helpdesk.models import Queue, Ticket
User = get_user_model() User = get_user_model()
@override_settings( @override_settings(HELPDESK_VIEW_A_TICKET_PUBLIC=True)
HELPDESK_VIEW_A_TICKET_PUBLIC=True
)
class TestTicketLookupPublicEnabled(TestCase): class TestTicketLookupPublicEnabled(TestCase):
def setUp(self): def setUp(self):
q = Queue(title='Q1', slug='q1') q = Queue(title="Q1", slug="q1")
q.save() 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.queue = q
t.save() t.save()
self.ticket = t self.ticket = t
@ -33,20 +31,26 @@ class TestTicketLookupPublicEnabled(TestCase):
# we will exercise 'reverse' to lookup/build the URL # we will exercise 'reverse' to lookup/build the URL
# from the ticket info we have # from the ticket info we have
# http://example.com/helpdesk/view/?ticket=q1-1&email=None # http://example.com/helpdesk/view/?ticket=q1-1&email=None
response = self.client.get(reverse('helpdesk:public_view'), response = self.client.get(
{'ticket': self.ticket.ticket_for_url, reverse("helpdesk:public_view"),
'email': self.ticket.submitter_email}) {
"ticket": self.ticket.ticket_for_url,
"email": self.ticket.submitter_email,
},
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_ticket_with_changed_queue(self): def test_ticket_with_changed_queue(self):
# Make a ticket (already done in setup() ) # Make a ticket (already done in setup() )
# Now make another queue # Now make another queue
q2 = Queue(title='Q2', slug='q2') q2 = Queue(title="Q2", slug="q2")
q2.save() q2.save()
# grab the URL / params which would have been emailed out to submitter. # grab the URL / params which would have been emailed out to submitter.
url = reverse('helpdesk:public_view') url = reverse("helpdesk:public_view")
params = {'ticket': self.ticket.ticket_for_url, params = {
'email': self.ticket.submitter_email} "ticket": self.ticket.ticket_for_url,
"email": self.ticket.submitter_email,
}
# Pickup the ticket created in setup() and change its queue # Pickup the ticket created in setup() and change its queue
self.ticket.queue = q2 self.ticket.queue = q2
self.ticket.save() self.ticket.save()
@ -56,36 +60,34 @@ class TestTicketLookupPublicEnabled(TestCase):
self.assertNotContains(response, "Invalid ticket ID") self.assertNotContains(response, "Invalid ticket ID")
def test_add_email_to_ticketcc_if_not_in(self): 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( 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.assigned_to = staff_user
self.ticket.save() self.ticket.save()
email_1 = 'user1@mail.com' email_1 = "user1@mail.com"
ticketcc_1 = self.ticket.ticketcc_set.create(email=email_1) ticketcc_1 = self.ticket.ticketcc_set.create(email=email_1)
# Add new email to CC # 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) ticketcc_2 = self.ticket.add_email_to_ticketcc_if_not_in(email=email_2)
self.assertEqual(list(self.ticket.ticketcc_set.all()), self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
[ticketcc_1, ticketcc_2])
# Add existing email, doesn't change anything # Add existing email, doesn't change anything
self.ticket.add_email_to_ticketcc_if_not_in(email=email_1) self.ticket.add_email_to_ticketcc_if_not_in(email=email_1)
self.assertEqual(list(self.ticket.ticketcc_set.all()), self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
[ticketcc_1, ticketcc_2])
# Add mail from assigned user, doesn't change anything # Add mail from assigned user, doesn't change anything
self.ticket.add_email_to_ticketcc_if_not_in(email=staff_email) self.ticket.add_email_to_ticketcc_if_not_in(email=staff_email)
self.assertEqual(list(self.ticket.ticketcc_set.all()), self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
[ticketcc_1, ticketcc_2])
self.ticket.add_email_to_ticketcc_if_not_in(user=staff_user) self.ticket.add_email_to_ticketcc_if_not_in(user=staff_user)
self.assertEqual(list(self.ticket.ticketcc_set.all()), self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
[ticketcc_1, ticketcc_2])
# Move a ticketCC from ticket 1 to ticket 2 # Move a ticketCC from ticket 1 to ticket 2
ticket_2 = Ticket.objects.create( 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) self.assertEqual(ticket_2.ticketcc_set.count(), 0)
ticket_2.add_email_to_ticketcc_if_not_in(ticketcc=ticketcc_1) ticket_2.add_email_to_ticketcc_if_not_in(ticketcc=ticketcc_1)
self.assertEqual(ticketcc_1.ticket, ticket_2) self.assertEqual(ticketcc_1.ticket, ticket_2)

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
import datetime import datetime
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -7,19 +6,19 @@ from django.test.client import Client
from helpdesk.models import FollowUp, Queue, Ticket from helpdesk.models import FollowUp, Queue, Ticket
import uuid import uuid
class TimeSpentTestCase(TestCase):
class TimeSpentTestCase(TestCase):
def setUp(self): def setUp(self):
self.queue_public = Queue.objects.create( self.queue_public = Queue.objects.create(
title='Queue 1', title="Queue 1",
slug='q1', slug="q1",
allow_public_submission=True, allow_public_submission=True,
dedicated_time=datetime.timedelta(minutes=60) dedicated_time=datetime.timedelta(minutes=60),
) )
self.ticket_data = { self.ticket_data = {
'title': 'Test Ticket', "title": "Test Ticket",
'description': 'Some Test Ticket', "description": "Some Test Ticket",
} }
ticket_data = dict(queue=self.queue_public, **self.ticket_data) ticket_data = dict(queue=self.queue_public, **self.ticket_data)
@ -28,12 +27,12 @@ class TimeSpentTestCase(TestCase):
self.client = Client() self.client = Client()
user1_kwargs = { user1_kwargs = {
'username': 'staff', "username": "staff",
'email': 'staff@example.com', "email": "staff@example.com",
'password': make_password('Test1234'), "password": make_password("Test1234"),
'is_staff': True, "is_staff": True,
'is_superuser': False, "is_superuser": False,
'is_active': True "is_active": True,
} }
self.user = User.objects.create(**user1_kwargs) self.user = User.objects.create(**user1_kwargs)
@ -50,7 +49,7 @@ class TimeSpentTestCase(TestCase):
user=self.user, user=self.user,
new_status=1, new_status=1,
message_id=message_id, message_id=message_id,
time_spent=datetime.timedelta(minutes=30) time_spent=datetime.timedelta(minutes=30),
) )
followup.save() followup.save()
@ -59,5 +58,6 @@ class TimeSpentTestCase(TestCase):
self.assertEqual(self.ticket.time_spent.seconds, 1800) self.assertEqual(self.ticket.time_spent.seconds, 1800)
self.assertEqual(self.queue_public.time_spent.seconds, 1800) self.assertEqual(self.queue_public.time_spent.seconds, 1800)
self.assertTrue( 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
) )

View File

@ -1,4 +1,3 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -13,43 +12,42 @@ import uuid
@override_settings(USE_TZ=True) @override_settings(USE_TZ=True)
class TimeSpentAutoTestCase(TestCase): class TimeSpentAutoTestCase(TestCase):
def setUp(self): def setUp(self):
"""Creates a queue, ticket and user.""" """Creates a queue, ticket and user."""
self.queue_public = Queue.objects.create( self.queue_public = Queue.objects.create(
title='Queue 1', title="Queue 1",
slug='q1', slug="q1",
allow_public_submission=True, allow_public_submission=True,
dedicated_time=timedelta(minutes=60) dedicated_time=timedelta(minutes=60),
) )
self.ticket_data = dict(queue=self.queue_public, self.ticket_data = dict(
title='test ticket', queue=self.queue_public,
description='test ticket description') title="test ticket",
description="test ticket description",
)
self.user = User.objects.create( self.user = User.objects.create(
username='staff', username="staff",
email='staff@example.com', email="staff@example.com",
password=make_password('Test1234'), password=make_password("Test1234"),
is_staff=True, is_staff=True,
is_superuser=False, is_superuser=False,
is_active=True is_active=True,
) )
self.client = Client() self.client = Client()
def loginUser(self, is_staff=True): def loginUser(self, is_staff=True):
"""Create a staff user and login""" """Create a staff user and login"""
User = get_user_model() User = get_user_model()
self.user = User.objects.create( self.user = User.objects.create(
username='User_1', username="User_1",
is_staff=is_staff, is_staff=is_staff,
) )
self.user.set_password('pass') self.user.set_password("pass")
self.user.save() 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): def test_add_two_followups_time_spent_auto(self):
"""Tests automatic time_spent calculation.""" """Tests automatic time_spent calculation."""
@ -59,15 +57,39 @@ class TimeSpentAutoTestCase(TestCase):
# ticket creation date, follow-up creation date, assertion value # ticket creation date, follow-up creation date, assertion value
TEST_VALUES = ( TEST_VALUES = (
# friday # 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-01T00:00:00+00:00', '2024-03-01T23:59:59+00:00', timedelta(hours=23, minutes=59, seconds=59)), "2024-03-01T09:30:10+00:00",
('2024-03-01T00:00:00+00:00', '2024-03-02T00:00:00+00:00', timedelta(hours=24)), timedelta(hours=9, minutes=30, seconds=10),
('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-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 # create and setup test ticket time
ticket = Ticket.objects.create(**self.ticket_data) ticket = Ticket.objects.create(**self.ticket_data)
ticket_time_p = datetime.strptime(ticket_time, "%Y-%m-%dT%H:%M:%S%z") ticket_time_p = datetime.strptime(ticket_time, "%Y-%m-%dT%H:%M:%S%z")
@ -85,15 +107,24 @@ class TimeSpentAutoTestCase(TestCase):
user=self.user, user=self.user,
new_status=1, new_status=1,
message_id=uuid.uuid4().hex, message_id=uuid.uuid4().hex,
time_spent=None time_spent=None,
) )
self.assertEqual(followup1.time_spent.total_seconds(), assertion_delta.total_seconds()) self.assertEqual(
self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds()) 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 # 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( followup2 = FollowUp.objects.create(
ticket=ticket, ticket=ticket,
date=followup1.date + delta, date=followup1.date + delta,
@ -103,16 +134,20 @@ class TimeSpentAutoTestCase(TestCase):
user=self.user, user=self.user,
new_status=1, new_status=1,
message_id=uuid.uuid4().hex, message_id=uuid.uuid4().hex,
time_spent=None time_spent=None,
) )
self.assertEqual(followup2.time_spent.total_seconds(), delta.total_seconds()) self.assertEqual(
self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds() + delta.total_seconds()) 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 # delete second follow-up as we test it with many intervals
followup2.delete() followup2.delete()
def test_followup_time_spent_auto_opening_hours(self): def test_followup_time_spent_auto_opening_hours(self):
"""Tests automatic time_spent calculation with opening hours and holidays.""" """Tests automatic time_spent calculation with opening hours and holidays."""
@ -130,45 +165,118 @@ class TimeSpentAutoTestCase(TestCase):
# adding holidays # adding holidays
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_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 # ticket creation date, follow-up creation date, assertion value
TEST_VALUES = ( TEST_VALUES = (
# monday # 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 # 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-05T07:00:00+00:00",
('2024-03-05T17:50:00+00:00', '2024-03-05T19:51:00+00:00', timedelta(minutes=10)), "2024-03-05T09:00:00+00:00",
('2024-03-05T18:00:00+00:00', '2024-03-05T23:59:59+00:00', timedelta(hours=0)), timedelta(hours=1),
('2024-03-05T20:00:00+00:00', '2024-03-05T20:59:59+00:00', timedelta(hours=0)), ),
(
"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 # 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-06T08:00:00+00:00",
('2024-03-06T18:01:00+00:00', '2024-03-06T19:00:00+00:00', timedelta(minutes=29)), "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 # 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 # 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 # 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 # 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 # 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 # 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 # 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 # create and setup test ticket time
ticket = Ticket.objects.create(**self.ticket_data) ticket = Ticket.objects.create(**self.ticket_data)
ticket_time_p = datetime.strptime(ticket_time, "%Y-%m-%dT%H:%M:%S%z") ticket_time_p = datetime.strptime(ticket_time, "%Y-%m-%dT%H:%M:%S%z")
@ -186,11 +294,15 @@ class TimeSpentAutoTestCase(TestCase):
user=self.user, user=self.user,
new_status=1, new_status=1,
message_id=uuid.uuid4().hex, message_id=uuid.uuid4().hex,
time_spent=None time_spent=None,
) )
self.assertEqual(followup1.time_spent.total_seconds(), assertion_delta.total_seconds()) self.assertEqual(
self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds()) 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 # removing opening hours and holidays
helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS = {} helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS = {}
@ -205,15 +317,18 @@ class TimeSpentAutoTestCase(TestCase):
# Follow-ups with OPEN_STATUS are excluded from time counting # Follow-ups with OPEN_STATUS are excluded from time counting
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = (Ticket.OPEN_STATUS,) helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = (Ticket.OPEN_STATUS,)
# create and setup test ticket time # create and setup test ticket time
ticket = Ticket.objects.create(**self.ticket_data) 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.created = ticket_time_p
ticket.modified = ticket_time_p ticket.modified = ticket_time_p
ticket.save() 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( followup1 = FollowUp.objects.create(
ticket=ticket, ticket=ticket,
date=fup_time_p, date=fup_time_p,
@ -223,7 +338,7 @@ class TimeSpentAutoTestCase(TestCase):
user=self.user, user=self.user,
new_status=1, new_status=1,
message_id=uuid.uuid4().hex, 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 # 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 # Remove status exclusion
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = () helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = ()
def test_followup_time_spent_auto_exclude_queues(self): def test_followup_time_spent_auto_exclude_queues(self):
"""Tests automatic time_spent calculation queues exclusion.""" """Tests automatic time_spent calculation queues exclusion."""
@ -241,17 +355,20 @@ class TimeSpentAutoTestCase(TestCase):
helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True
# Follow-ups within the default queue are excluded from time counting # 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 # create and setup test ticket time
ticket = Ticket.objects.create(**self.ticket_data) 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.created = ticket_time_p
ticket.modified = ticket_time_p ticket.modified = ticket_time_p
ticket.save() 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( followup1 = FollowUp.objects.create(
ticket=ticket, ticket=ticket,
date=fup_time_p, date=fup_time_p,
@ -261,7 +378,7 @@ class TimeSpentAutoTestCase(TestCase):
user=self.user, user=self.user,
new_status=1, new_status=1,
message_id=uuid.uuid4().hex, 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 # 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 # activate automatic calculation
helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True 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 # make staff user
self.loginUser() self.loginUser()
# create queues # create queues
queues_sequence = ('new', 'stop1', 'resume1', 'stop2', 'resume2', 'end') queues_sequence = ("new", "stop1", "resume1", "stop2", "resume2", "end")
queues = dict() queues = dict()
for slug in queues_sequence: for slug in queues_sequence:
queues[slug] = Queue.objects.create( queues[slug] = Queue.objects.create(
@ -292,34 +409,39 @@ class TimeSpentAutoTestCase(TestCase):
# create ticket # create ticket
initial_data = { initial_data = {
'title': 'Queue change ticket test', "title": "Queue change ticket test",
'queue': queues['new'], "queue": queues["new"],
'assigned_to': self.user, "assigned_to": self.user,
'status': Ticket.OPEN_STATUS, "status": Ticket.OPEN_STATUS,
'created': datetime.strptime('2024-04-09T08:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z") "created": datetime.strptime(
"2024-04-09T08:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"
),
} }
ticket = Ticket.objects.create(**initial_data) ticket = Ticket.objects.create(**initial_data)
# create a change queue follow-up every hour # create a change queue follow-up every hour
# first follow-up created at the same time of the ticket without queue change # 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 # 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 # create follow-up
post_data = { post_data = {
'comment': 'ticket in queue {}'.format(queue), "comment": "ticket in queue {}".format(queue),
'queue': queues[queue].id, "queue": queues[queue].id,
} }
response = self.client.post(reverse('helpdesk:update', kwargs={ response = self.client.post(
'ticket_id': ticket.id}), post_data) reverse("helpdesk:update", kwargs={"ticket_id": ticket.id}), post_data
latest_fup = ticket.followup_set.latest('id') )
latest_fup = ticket.followup_set.latest("id")
latest_fup.date = ticket.created + timedelta(hours=i) latest_fup.date = ticket.created + timedelta(hours=i)
latest_fup.time_spent = None latest_fup.time_spent = None
latest_fup.save() latest_fup.save()
# total ticket time for followups is 5 hours # total ticket time for followups is 5 hours
self.assertEqual(latest_fup.date - ticket.created, timedelta(hours=5)) self.assertEqual(latest_fup.date - ticket.created, timedelta(hours=5))
# calculated time spent with 2 hours exclusion is 3 hours # 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 # remove queues exclusion
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = () helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ()

View File

@ -4,20 +4,18 @@ from django.urls import reverse
class TicketActionsTestCase(TestCase): class TicketActionsTestCase(TestCase):
fixtures = ['emailtemplate.json'] fixtures = ["emailtemplate.json"]
def setUp(self): def setUp(self):
User = get_user_model() User = get_user_model()
self.user = User.objects.create( self.user = User.objects.create(
username='User_1', username="User_1",
is_staff=True, is_staff=True,
) )
self.user.set_password('pass') self.user.set_password("pass")
self.user.save() 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): 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") self.assertContains(response, "Use the following options")

View File

@ -1,9 +1,7 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from helpdesk.models import Queue, CustomField, TicketCustomFieldValue, Ticket from helpdesk.models import Queue, CustomField, TicketCustomFieldValue, Ticket
from helpdesk.serializers import TicketSerializer from helpdesk.serializers import TicketSerializer
from rest_framework.status import ( from rest_framework.status import HTTP_201_CREATED
HTTP_201_CREATED
)
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
import json import json
import os import os
@ -15,42 +13,53 @@ import http.server
import threading import threading
from http import HTTPStatus from http import HTTPStatus
class WebhookRequestHandler(http.server.BaseHTTPRequestHandler): class WebhookRequestHandler(http.server.BaseHTTPRequestHandler):
server: "WebhookServer" server: "WebhookServer"
def do_POST(self): def do_POST(self):
content_length = int(self.headers['Content-Length']) content_length = int(self.headers["Content-Length"])
body = self.rfile.read(content_length) body = self.rfile.read(content_length)
self.server.requests.append({ self.server.requests.append(
'path': self.path, {"path": self.path, "headers": self.headers, "body": body}
'headers': self.headers, )
'body': body if self.path == "/new-ticket":
}) self.server.handled_new_ticket_requests.append(
if self.path == '/new-ticket': json.loads(body.decode("utf-8"))
self.server.handled_new_ticket_requests.append(json.loads(body.decode('utf-8'))) )
if self.path == '/new-ticket-1': if self.path == "/new-ticket-1":
self.server.handled_new_ticket_requests_1.append(json.loads(body.decode('utf-8'))) self.server.handled_new_ticket_requests_1.append(
elif self.path == '/followup': json.loads(body.decode("utf-8"))
self.server.handled_follow_up_requests.append(json.loads(body.decode('utf-8'))) )
elif self.path == '/followup-1': elif self.path == "/followup":
self.server.handled_follow_up_requests_1.append(json.loads(body.decode('utf-8'))) 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.send_response(HTTPStatus.OK)
self.end_headers() self.end_headers()
def do_GET(self): 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.send_response(HTTPStatus.NOT_FOUND)
self.end_headers() self.end_headers()
return return
self.send_response(HTTPStatus.OK) self.send_response(HTTPStatus.OK)
self.send_header('Content-type', 'application/json') self.send_header("Content-type", "application/json")
self.end_headers() self.end_headers()
self.wfile.write(json.dumps({ self.wfile.write(
'new_ticket_requests': self.server.handled_new_ticket_requests, json.dumps(
'new_ticket_requests_1': self.server.handled_new_ticket_requests_1, {
'follow_up_requests': self.server.handled_follow_up_requests, "new_ticket_requests": self.server.handled_new_ticket_requests,
'follow_up_requests_1': self.server.handled_follow_up_requests_1 "new_ticket_requests_1": self.server.handled_new_ticket_requests_1,
}).encode('utf-8')) "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): class WebhookServer(http.server.HTTPServer):
@ -64,7 +73,9 @@ class WebhookServer(http.server.HTTPServer):
def start(self): def start(self):
self.thread = threading.Thread(target=self.serve_forever) 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() self.thread.start()
def stop(self): def stop(self):
@ -77,12 +88,12 @@ class WebhookTest(APITestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
cls.queue = Queue.objects.create( cls.queue = Queue.objects.create(
title='Test Queue', title="Test Queue",
slug='test-queue', slug="test-queue",
) )
def setUp(self): 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( CustomField(
name="my_custom_field", name="my_custom_field",
data_type="varchar", data_type="varchar",
@ -91,82 +102,135 @@ class WebhookTest(APITestCase):
self.client.force_authenticate(staff_user) self.client.force_authenticate(staff_user)
def test_test_server(self): def test_test_server(self):
server = WebhookServer(('localhost', 8123), WebhookRequestHandler) server = WebhookServer(("localhost", 8123), WebhookRequestHandler)
server.start() server.start()
requests.post('http://localhost:8123/new-ticket', json={ requests.post("http://localhost:8123/new-ticket", json={"foo": "bar"})
"foo": "bar"}) handled_webhook_requests = requests.get(
handled_webhook_requests = requests.get('http://localhost:8123/get-past-requests').json() "http://localhost:8123/get-past-requests"
self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["foo"], "bar") ).json()
self.assertEqual(
handled_webhook_requests["new_ticket_requests"][-1]["foo"], "bar"
)
server.stop() server.stop()
def test_create_ticket_and_followup_via_api(self): def test_create_ticket_and_followup_via_api(self):
server = WebhookServer(('localhost', 8124), WebhookRequestHandler) 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_NEW_TICKET_WEBHOOK_URLS"] = (
os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8124/followup , http://localhost:8124/followup-1' "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() server.start()
response = self.client.post('/api/tickets/', { response = self.client.post(
'queue': self.queue.id, "/api/tickets/",
'title': 'Test title', {
'description': 'Test description\nMulti lines', "queue": self.queue.id,
'submitter_email': 'test@mail.com', "title": "Test title",
'priority': 4, "description": "Test description\nMulti lines",
'custom_my_custom_field': 'custom value', "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(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) 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() 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)
self.assertTrue(len(handled_webhook_requests['new_ticket_requests_1']) == 1) self.assertTrue(len(handled_webhook_requests["new_ticket_requests_1"]) == 1)
self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 0) 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(
self.assertEqual(handled_webhook_requests['new_ticket_requests_1'][-1]["ticket"]["title"], "Test title") handled_webhook_requests["new_ticket_requests"][-1]["ticket"]["title"],
self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["description"], "Test description\nMulti lines") "Test title",
ticket = Ticket.objects.get(id=handled_webhook_requests["new_ticket_requests"][-1]["ticket"]["id"]) )
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() ticket.set_custom_field_values()
serializer = TicketSerializer(ticket) serializer = TicketSerializer(ticket)
self.assertEqual( self.assertEqual(
list(sorted(serializer.fields.keys())), list(sorted(serializer.fields.keys())),
['assigned_to', [
'attachment', "assigned_to",
'custom_my_custom_field', "attachment",
'description', "custom_my_custom_field",
'due_date', "description",
'followup_set', "due_date",
'id', "followup_set",
'merged_to', "id",
'on_hold', "merged_to",
'priority', "on_hold",
'queue', "priority",
'resolution', "queue",
'status', "resolution",
'submitter_email', "status",
'title'] "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) 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() 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)
self.assertEqual(len(handled_webhook_requests['follow_up_requests_1']), 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(
self.assertEqual(handled_webhook_requests['follow_up_requests_1'][-1]["ticket"]["followup_set"][-1]["comment"], "Test comment") 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() server.stop()
def test_create_ticket_and_followup_via_email(self): def test_create_ticket_and_followup_via_email(self):
from .. import email from .. import email
server = WebhookServer(('localhost', 8125), WebhookRequestHandler) server = WebhookServer(("localhost", 8125), WebhookRequestHandler)
os.environ['HELPDESK_NEW_TICKET_WEBHOOK_URLS'] = 'http://localhost:8125/new-ticket' os.environ["HELPDESK_NEW_TICKET_WEBHOOK_URLS"] = (
os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8125/followup' "http://localhost:8125/new-ticket"
)
os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = "http://localhost:8125/followup"
server.start() server.start()
class MockMessage(dict): class MockMessage(dict):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
@ -175,13 +239,13 @@ class WebhookTest(APITestCase):
return self.__dict__.get(key, default) return self.__dict__.get(key, default)
payload = { payload = {
'body': "hello", "body": "hello",
'full_body': "hello", "full_body": "hello",
'subject': "Test subject", "subject": "Test subject",
'queue': self.queue, "queue": self.queue,
'sender_email': "user@example.com", "sender_email": "user@example.com",
'priority': "1", "priority": "1",
'files': [], "files": [],
} }
message = { message = {
@ -195,26 +259,29 @@ class WebhookTest(APITestCase):
ticket_id=None, ticket_id=None,
payload=payload, payload=payload,
files=[], 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() handled_webhook_requests = handled_webhook_requests.json()
self.assertEqual(len(handled_webhook_requests['new_ticket_requests']), 1) 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["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 from .. import models
ticket = models.Ticket.objects.get(id=ticket_id) ticket = models.Ticket.objects.get(id=ticket_id)
payload = { payload = {
'body': "hello", "body": "hello",
'full_body': "hello", "full_body": "hello",
'subject': f"[test-queue-{ticket_id}] Test subject", "subject": f"[test-queue-{ticket_id}] Test subject",
'queue': self.queue, "queue": self.queue,
'sender_email': "user@example.com", "sender_email": "user@example.com",
'priority': "1", "priority": "1",
'files': [], "files": [],
} }
message = { message = {
@ -228,12 +295,22 @@ class WebhookTest(APITestCase):
ticket_id=ticket_id, ticket_id=ticket_id,
payload=payload, payload=payload,
files=[], 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() 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)
self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["followup_set"][-1]["comment"], "hello") self.assertEqual(
self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["id"], ticket_id) 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() server.stop()

View File

@ -3,6 +3,6 @@ from django.urls import include, path
urlpatterns = [ urlpatterns = [
path('', include('helpdesk.urls', namespace='helpdesk')), path("", include("helpdesk.urls", namespace="helpdesk")),
path('admin/', admin.site.urls), path("admin/", admin.site.urls),
] ]

View File

@ -1,6 +1,5 @@
"""UItility functions facilitate making unit testing easier and less brittle.""" """UItility functions facilitate making unit testing easier and less brittle."""
from PIL import Image from PIL import Image
import email import email
from email import encoders from email import encoders
@ -31,8 +30,8 @@ def strip_accents(text):
:returns: The processed String. :returns: The processed String.
:rtype: String. :rtype: String.
""" """
text = unicodedata.normalize('NFD', text) text = unicodedata.normalize("NFD", text)
text = text.encode('ascii', 'ignore') text = text.encode("ascii", "ignore")
text = text.decode("utf-8") text = text.decode("utf-8")
return str(text) return str(text)
@ -48,12 +47,12 @@ def text_to_id(text):
:rtype: String. :rtype: String.
""" """
text = strip_accents(text.lower()) text = strip_accents(text.lower())
text = re.sub('[ ]+', '_', text) text = re.sub("[ ]+", "_", text)
text = re.sub('[^0-9a-zA-Z_-]', '', text) text = re.sub("[^0-9a-zA-Z_-]", "", text)
return text return text
def get_random_string(length: int=16) -> str: def get_random_string(length: int = 16) -> str:
return "".join( return "".join(
[random.choice(string.ascii_letters + string.digits) for _ in range(length)] [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): def generate_random_image(image_format, array_dims):
""" """
Creates an image from a random array. Creates an image from a random array.
:param image_format: An image format (PNG or JPEG). :param image_format: An image format (PNG or JPEG).
:param array_dims: A tuple with array dimensions. :param array_dims: A tuple with array dimensions.
:returns: A byte string with encoded image :returns: A byte string with encoded image
:rtype: bytes :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() io = BytesIO()
image_pil = Image.fromarray(image_bytes) image_pil = Image.fromarray(image_bytes)
image_pil.save(io, image_format, subsampling=0, quality=100) image_pil.save(io, image_format, subsampling=0, quality=100)
return io.getvalue() 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. Returns a random image.
Args: Args:
image_format: An image format (PNG or JPEG). image_format: An image format (PNG or JPEG).
Returns: Returns:
A string with encoded image 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: def get_fake(provider: str, locale: str = "en_US", min_length: int = 5) -> Any:
""" """
Generates a random string, float, integer etc based on provider Generates a random string, float, integer etc based on provider
Provider can be "text', 'sentence', "word" Provider can be "text', 'sentence', "word"
e.g. `get_fake('name')` ==> 'Buzz Aldrin' 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: while len(string) < min_length:
string += factory.Faker(provider).evaluate({}, None, {'locale': locale,}) string += factory.Faker(provider).evaluate(
{},
None,
{
"locale": locale,
},
)
return string return string
def get_fake_html(locale: str = "en_US", wrap_in_body_tag=True) -> Any: def get_fake_html(locale: str = "en_US", wrap_in_body_tag=True) -> Any:
""" """
Generates a random string, float, integer etc based on provider Generates a random string, float, integer etc based on provider
Provider can be "text', 'sentence', Provider can be "text', 'sentence',
e.g. `get_fake('name')` ==> 'Buzz Aldrin' e.g. `get_fake('name')` ==> 'Buzz Aldrin'
""" """
html = factory.Faker("sentence").evaluate({}, None, {'locale': locale,}) html = factory.Faker("sentence").evaluate(
for _ in range(0,4): {},
html += "<li>" + factory.Faker("sentence").evaluate({}, None, {'locale': locale,}) + "</li>" None,
for _ in range(0,4): {
html += "<p>" + factory.Faker("text").evaluate({}, None, {'locale': locale,}) "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 return f"<body>{html}</body>" if wrap_in_body_tag else html
def generate_email_address( def generate_email_address(
locale: str="en_US", locale: str = "en_US",
use_short_email: bool=False, use_short_email: bool = False,
real_name_format: Optional[str]="{last_name}, {first_name}", real_name_format: Optional[str] = "{last_name}, {first_name}",
last_name_override: Optional[str]=None) -> Tuple[str, str, str, str]: last_name_override: Optional[str] = None,
''' ) -> Tuple[str, str, str, str]:
"""
Generate an RFC 2822 email address Generate an RFC 2822 email address
:param locale: change this to generate locale specific names :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 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 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 :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 :returns <RFC2822 formatted email for header>, <short email address>, <first name>, <last_name
''' """
fake = faker.Faker(locale=locale) fake = faker.Faker(locale=locale)
first_name = fake.first_name() first_name = fake.first_name()
last_name = last_name_override or fake.last_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 # Add a random string to ensure we do not generate a real domain name
email_address = "{}.{}@{}".format( email_address = "{}.{}@{}".format(
first_name.replace(' ', '').encode("ascii", "ignore").lower().decode(), first_name.replace(" ", "").encode("ascii", "ignore").lower().decode(),
last_name.replace(' ', '').encode("ascii", "ignore").lower().decode(), last_name.replace(" ", "").encode("ascii", "ignore").lower().decode(),
get_random_string(5) + fake.domain_name() get_random_string(5) + fake.domain_name(),
) )
# format email address for RFC 2822 and return # 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 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 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 :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 = MIMEBase("application", "octet-stream")
part.set_payload(get_fake("text", locale=locale, min_length=1024) if content is None else content) part.set_payload(
get_fake("text", locale=locale, min_length=1024) if content is None else content
)
encoders.encode_base64(part) encoders.encode_base64(part)
if not filename: if not filename:
filename = get_fake("word", locale=locale, min_length=8) + ".txt" 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 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 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 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 :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 = MIMEBase("application", "vnd.microsoft.portable-executable")
part.set_payload(get_fake("text", locale=locale, min_length=1024) if content is None else content) part.set_payload(
get_fake("text", locale=locale, min_length=1024) if content is None else content
)
encoders.encode_base64(part) encoders.encode_base64(part)
if not filename: if not filename:
filename = get_fake("word", locale=locale, min_length=8) + ".exe" 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 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 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 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 = 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) encoders.encode_base64(part)
if not imagename: if not imagename:
imagename = get_fake("word", locale=locale, min_length=8) + ".jpg" 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 return part
def generate_email_list(address_cnt: int = 3,
locale: str="en_US", def generate_email_list(
use_short_email: bool=False address_cnt: int = 3, locale: str = "en_US", use_short_email: bool = False
) -> str: ) -> str:
""" """
Generates a list of email addresses formatted for email headers on a Mime part 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 address_cnt: the number of email addresses to string together
:param locale: change this to generate locale specific "real names" and subject :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 :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) return ",".join(email_address_list)
def add_simple_email_headers(message: Message, locale: str="en_US",
use_short_email: bool=False def add_simple_email_headers(
) -> typing.Tuple[typing.Tuple[str, str], typing.Tuple[str, str]]: 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 Adds the key email headers to a Mime part
:param message: the Mime part to add headers to :param message: the Mime part to add headers to
:param locale: change this to generate locale specific "real names" and subject :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 :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) to_meta = generate_email_address(locale, use_short_email=use_short_email)
from_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["Subject"] = get_fake("sentence", locale=locale)
message['From'] = from_meta[0] message["From"] = from_meta[0]
message['To'] = to_meta[0] message["To"] = to_meta[0]
return from_meta, to_meta return from_meta, to_meta
def generate_mime_part(locale: str="en_US",
part_type: str="plain", def generate_mime_part(
) -> typing.Optional[Message]: locale: str = "en_US",
part_type: str = "plain",
) -> typing.Optional[Message]:
""" """
Generates amime part of the sepecified type Generates amime part of the sepecified type
:param locale: change this to generate locale specific strings :param locale: change this to generate locale specific strings
:param text_type: options are plain, html, image (attachment), file (attachment) :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) raise Exception("Mime part not implemented: " + part_type)
return msg return msg
def generate_multipart_email(locale: str="en_US",
type_list: typing.List[str]=["plain", "html", "image"], def generate_multipart_email(
sub_type: str = None, locale: str = "en_US",
use_short_email: bool=False type_list: typing.List[str] = ["plain", "html", "image"],
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]: 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 Generates an email including headers with the defined multiparts
:param locale: :param locale:
:param type_list: options are plain, html, image (attachment), file (attachment), and executable (.exe attachment) :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 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 :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() msg = MIMEMultipart(sub_type) if sub_type else MIMEMultipart()
for part_type in type_list: for part_type in type_list:
msg.attach(generate_mime_part(locale=locale, part_type=part_type)) 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 return msg, from_meta, to_meta
def generate_text_email(locale: str="en_US",
use_short_email: bool=False def generate_text_email(
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]: 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 Generates an email including headers
""" """
body = get_fake("text", locale=locale, min_length=1024) body = get_fake("text", locale=locale, min_length=1024)
msg = MIMEText(body) 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 return msg, from_meta, to_meta
def generate_html_email(locale: str="en_US",
use_short_email: bool=False def generate_html_email(
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]: 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 Generates an email including headers
""" """
body = get_fake_html(locale=locale) body = get_fake_html(locale=locale)
msg = MIMEText(body) 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 return msg, from_meta, to_meta

View File

@ -20,15 +20,15 @@ from helpdesk.signals import update_ticket_done
User = get_user_model() User = get_user_model()
def add_staff_subscription(
user: User, def add_staff_subscription(user: User, ticket: Ticket) -> None:
ticket: Ticket
) -> None:
"""Auto subscribe the staff member if that's what the settigs say and the """Auto subscribe the staff member if that's what the settigs say and the
user is authenticated and a staff member""" user is authenticated and a staff member"""
if helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE \ if (
and user.is_authenticated \ helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE
and return_ticketccstring_and_show_subscribe(user, ticket)[1]: and user.is_authenticated
and return_ticketccstring_and_show_subscribe(user, ticket)[1]
):
subscribe_to_ticket_updates(ticket, user) 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(username)
strings_to_check.append(useremail) strings_to_check.append(useremail)
ticketcc_string = '' ticketcc_string = ""
all_ticketcc = ticket.ticketcc_set.all() all_ticketcc = ticket.ticketcc_set.all()
counter_all_ticketcc = len(all_ticketcc) - 1 counter_all_ticketcc = len(all_ticketcc) - 1
show_subscribe = True show_subscribe = True
@ -53,7 +53,7 @@ def return_ticketccstring_and_show_subscribe(user, ticket):
ticketcc_this_entry = str(ticketcc.display) ticketcc_this_entry = str(ticketcc.display)
ticketcc_string += ticketcc_this_entry ticketcc_string += ticketcc_this_entry
if i < counter_all_ticketcc: if i < counter_all_ticketcc:
ticketcc_string += ', ' ticketcc_string += ", "
if strings_to_check.__contains__(ticketcc_this_entry.upper()): if strings_to_check.__contains__(ticketcc_this_entry.upper()):
show_subscribe = False show_subscribe = False
@ -64,18 +64,19 @@ def return_ticketccstring_and_show_subscribe(user, ticket):
submitter_email = ticket.submitter_email.upper() submitter_email = ticket.submitter_email.upper()
strings_to_check.append(submitter_email) strings_to_check.append(submitter_email)
strings_to_check.append(assignedto_username) 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 show_subscribe = False
return ticketcc_string, show_subscribe 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: 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 # Don't create duplicate entries for subscribers
if queryset.count() > 0: 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: if user is None and len(email) < 5:
raise ValidationError( 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( return ticket.ticketcc_set.create(
user=user, user=user, email=email, can_view=can_view, can_update=can_update
email=email,
can_view=can_view,
can_update=can_update
) )
def get_and_set_ticket_status( def get_and_set_ticket_status(
new_status: int, new_status: int, ticket: Ticket, follow_up: FollowUp
ticket: Ticket,
follow_up: FollowUp
) -> typing.Tuple[str, int]: ) -> typing.Tuple[str, int]:
"""Performs comparision on previous status to new status, """Performs comparision on previous status to new status,
updating the title as required. updating the title as required.
@ -112,15 +111,15 @@ def get_and_set_ticket_status(
ticket.save() ticket.save()
follow_up.new_status = new_status follow_up.new_status = new_status
if follow_up.title: if follow_up.title:
follow_up.title += ' and %s' % ticket.get_status_display() follow_up.title += " and %s" % ticket.get_status_display()
else: else:
follow_up.title = '%s' % ticket.get_status_display() follow_up.title = "%s" % ticket.get_status_display()
if not follow_up.title: if not follow_up.title:
if follow_up.comment: if follow_up.comment:
follow_up.title = _('Comment') follow_up.title = _("Comment")
else: else:
follow_up.title = _('Updated') follow_up.title = _("Updated")
follow_up.save() follow_up.save()
return old_status_str, old_status return old_status_str, old_status
@ -132,80 +131,76 @@ def update_messages_sent_to_by_public_and_status(
follow_up: FollowUp, follow_up: FollowUp,
context: str, context: str,
messages_sent_to: typing.Set[str], messages_sent_to: typing.Set[str],
files: typing.List[typing.Tuple[str, str]] files: typing.List[typing.Tuple[str, str]],
) -> Ticket: ) -> Ticket:
"""Sets the status of the ticket""" """Sets the status of the ticket"""
if public and ( if public and (
follow_up.comment or ( follow_up.comment
follow_up.new_status in ( or (follow_up.new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS))
Ticket.RESOLVED_STATUS,
Ticket.CLOSED_STATUS
)
)
): ):
if follow_up.new_status == Ticket.RESOLVED_STATUS: if follow_up.new_status == Ticket.RESOLVED_STATUS:
template = 'resolved_' template = "resolved_"
elif follow_up.new_status == Ticket.CLOSED_STATUS: elif follow_up.new_status == Ticket.CLOSED_STATUS:
template = 'closed_' template = "closed_"
else: else:
template = 'updated_' template = "updated_"
roles = { roles = {
'submitter': (template + 'submitter', context), "submitter": (template + "submitter", context),
'ticket_cc': (template + 'cc', context), "ticket_cc": (template + "cc", context),
} }
if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change: if (
roles['assigned_to'] = (template + 'cc', context) ticket.assigned_to
and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change
):
roles["assigned_to"] = (template + "cc", context)
messages_sent_to.update( messages_sent_to.update(
ticket.send( ticket.send(
roles, roles, dont_send_to=messages_sent_to, fail_silently=True, files=files
dont_send_to=messages_sent_to,
fail_silently=True,
files=files
) )
) )
return ticket return ticket
def get_template_staff_and_template_cc( def get_template_staff_and_template_cc(
reassigned, follow_up: FollowUp reassigned, follow_up: FollowUp
) -> typing.Tuple[str, str]: ) -> typing.Tuple[str, str]:
if reassigned: if reassigned:
template_staff = 'assigned_owner' template_staff = "assigned_owner"
elif follow_up.new_status == Ticket.RESOLVED_STATUS: elif follow_up.new_status == Ticket.RESOLVED_STATUS:
template_staff = 'resolved_owner' template_staff = "resolved_owner"
elif follow_up.new_status == Ticket.CLOSED_STATUS: elif follow_up.new_status == Ticket.CLOSED_STATUS:
template_staff = 'closed_owner' template_staff = "closed_owner"
else: else:
template_staff = 'updated_owner' template_staff = "updated_owner"
if reassigned: if reassigned:
template_cc = 'assigned_cc' template_cc = "assigned_cc"
elif follow_up.new_status == Ticket.RESOLVED_STATUS: elif follow_up.new_status == Ticket.RESOLVED_STATUS:
template_cc = 'resolved_cc' template_cc = "resolved_cc"
elif follow_up.new_status == Ticket.CLOSED_STATUS: elif follow_up.new_status == Ticket.CLOSED_STATUS:
template_cc = 'closed_cc' template_cc = "closed_cc"
else: else:
template_cc = 'updated_cc' template_cc = "updated_cc"
return template_staff, template_cc return template_staff, template_cc
def update_ticket( def update_ticket(
user, user,
ticket, ticket,
title=None, title=None,
comment="", comment="",
files=None, files=None,
public=False, public=False,
owner=-1, owner=-1,
priority=-1, priority=-1,
queue=-1, queue=-1,
new_status=None, new_status=None,
time_spent=None, time_spent=None,
due_date=None, due_date=None,
new_checklists=None, new_checklists=None,
message_id=None, message_id=None,
customfields_form=None, customfields_form=None,
): ):
# We need to allow the 'ticket' and 'queue' contexts to be applied to the # We need to allow the 'ticket' and 'queue' contexts to be applied to the
# comment. # comment.
@ -222,25 +217,31 @@ def update_ticket(
new_checklists = {} new_checklists = {}
from django.template import engines 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 # this prevents system from trying to render any template tags
# broken into two stages to prevent changes from first replace being themselves # broken into two stages to prevent changes from first replace being themselves
# changed by the second replace due to conflicting syntax # changed by the second replace due to conflicting syntax
comment = comment.replace( comment = comment.replace("{%", "X-HELPDESK-COMMENT-VERBATIM").replace(
'{%', 'X-HELPDESK-COMMENT-VERBATIM').replace('%}', 'X-HELPDESK-COMMENT-ENDVERBATIM') "%}", "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", "{% verbatim %}{%"
).replace("X-HELPDESK-COMMENT-ENDVERBATIM", "%}{% endverbatim %}")
# render the neutralized template # render the neutralized template
comment = template_func(comment).render(context) comment = template_func(comment).render(context)
if owner == -1 and ticket.assigned_to: if owner == -1 and ticket.assigned_to:
owner = ticket.assigned_to.id owner = ticket.assigned_to.id
f = FollowUp(ticket=ticket, date=timezone.now(), comment=comment, f = FollowUp(
time_spent=time_spent, message_id=message_id, title=title) ticket=ticket,
date=timezone.now(),
comment=comment,
time_spent=time_spent,
message_id=message_id,
title=title,
)
if is_helpdesk_staff(user): if is_helpdesk_staff(user):
f.user = user f.user = user
@ -251,16 +252,19 @@ def update_ticket(
old_owner = ticket.assigned_to old_owner = ticket.assigned_to
if owner != -1: 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) new_user = User.objects.get(id=owner)
f.title = _('Assigned to %(username)s') % { f.title = _("Assigned to %(username)s") % {
'username': new_user.get_username(), "username": new_user.get_username(),
} }
ticket.assigned_to = new_user ticket.assigned_to = new_user
reassigned = True reassigned = True
# user changed owner to 'unassign' # user changed owner to 'unassign'
elif owner == 0 and ticket.assigned_to is not None: elif owner == 0 and ticket.assigned_to is not None:
f.title = _('Unassigned') f.title = _("Unassigned")
ticket.assigned_to = None ticket.assigned_to = None
old_status_str, old_status = get_and_set_ticket_status(new_status, ticket, f) 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: if title and title != ticket.title:
c = f.ticketchange_set.create( c = f.ticketchange_set.create(
field=_('Title'), field=_("Title"),
old_value=ticket.title, old_value=ticket.title,
new_value=title, new_value=title,
) )
@ -277,21 +281,21 @@ def update_ticket(
if new_status != old_status: if new_status != old_status:
c = f.ticketchange_set.create( c = f.ticketchange_set.create(
field=_('Status'), field=_("Status"),
old_value=old_status_str, old_value=old_status_str,
new_value=ticket.get_status_display(), new_value=ticket.get_status_display(),
) )
if ticket.assigned_to != old_owner: if ticket.assigned_to != old_owner:
c = f.ticketchange_set.create( c = f.ticketchange_set.create(
field=_('Owner'), field=_("Owner"),
old_value=old_owner, old_value=old_owner,
new_value=ticket.assigned_to, new_value=ticket.assigned_to,
) )
if priority != ticket.priority: if priority != ticket.priority:
c = f.ticketchange_set.create( c = f.ticketchange_set.create(
field=_('Priority'), field=_("Priority"),
old_value=ticket.priority, old_value=ticket.priority,
new_value=priority, new_value=priority,
) )
@ -299,7 +303,7 @@ def update_ticket(
if queue != ticket.queue.id: if queue != ticket.queue.id:
c = f.ticketchange_set.create( c = f.ticketchange_set.create(
field=_('Queue'), field=_("Queue"),
old_value=ticket.queue.id, old_value=ticket.queue.id,
new_value=queue, new_value=queue,
) )
@ -307,16 +311,16 @@ def update_ticket(
if due_date != ticket.due_date: if due_date != ticket.due_date:
c = f.ticketchange_set.create( c = f.ticketchange_set.create(
field=_('Due on'), field=_("Due on"),
old_value=ticket.due_date, old_value=ticket.due_date,
new_value=due_date, new_value=due_date,
) )
ticket.due_date = due_date ticket.due_date = due_date
# save custom fields and ticket changes # save custom fields and ticket changes
if customfields_form and customfields_form.is_valid(): if customfields_form and customfields_form.is_valid():
customfields_form.save(followup=f) customfields_form.save(followup=f)
for checklist in ticket.checklists.all(): for checklist in ticket.checklists.all():
if checklist.id not in new_checklists: if checklist.id not in new_checklists:
continue continue
@ -327,24 +331,22 @@ def update_ticket(
# Add completion if it was not done yet # Add completion if it was not done yet
if not task.completion_date and task.id in new_completed_tasks: if not task.completion_date and task.id in new_completed_tasks:
task.completion_date = timezone.now() task.completion_date = timezone.now()
changed = 'completed' changed = "completed"
# Remove it if it was done before # Remove it if it was done before
elif task.completion_date and task.id not in new_completed_tasks: elif task.completion_date and task.id not in new_completed_tasks:
task.completion_date = None task.completion_date = None
changed = 'uncompleted' changed = "uncompleted"
# Save and add ticket change if task state has changed # Save and add ticket change if task state has changed
if changed: if changed:
task.save(update_fields=['completion_date']) task.save(update_fields=["completion_date"])
f.ticketchange_set.create( f.ticketchange_set.create(
field=f'[{checklist.name}] {task.description}', field=f"[{checklist.name}] {task.description}",
old_value=_('To do') if changed == 'completed' else _('Completed'), old_value=_("To do") if changed == "completed" else _("Completed"),
new_value=_('Completed') if changed == 'completed' else _('To do'), new_value=_("Completed") if changed == "completed" else _("To do"),
) )
if new_status in ( if new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS) and (
Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS
) and (
new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None
): ):
ticket.resolution = comment ticket.resolution = comment
@ -363,32 +365,34 @@ def update_ticket(
except AttributeError: except AttributeError:
pass pass
ticket = update_messages_sent_to_by_public_and_status( ticket = update_messages_sent_to_by_public_and_status(
public, public, ticket, f, context, messages_sent_to, files
ticket,
f,
context,
messages_sent_to,
files
) )
template_staff, template_cc = get_template_staff_and_template_cc(reassigned, f) template_staff, template_cc = get_template_staff_and_template_cc(reassigned, f)
if ticket.assigned_to and ( if ticket.assigned_to and (
ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change 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( messages_sent_to.update(
{'assigned_to': (template_staff, context)}, 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, dont_send_to=messages_sent_to,
fail_silently=True, fail_silently=True,
files=files, 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() ticket.save()
# emit signal with followup when the ticket update is done # emit signal with followup when the ticket update is done
@ -398,4 +402,3 @@ def update_ticket(
# auto subscribe user if enabled # auto subscribe user if enabled
add_staff_subscription(user, ticket) add_staff_subscription(user, ticket)
return f return f

View File

@ -14,7 +14,13 @@ from django.views.generic import TemplateView
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.decorators import helpdesk_staff_member_required, protect_view from helpdesk.decorators import helpdesk_staff_member_required, protect_view
from helpdesk.views import feeds, login, public, staff 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 from rest_framework.routers import DefaultRouter
@ -63,16 +69,12 @@ urlpatterns = [
name="followup_delete", name="followup_delete",
), ),
path("tickets/<int:ticket_id>/edit/", staff.edit_ticket, name="edit"), path("tickets/<int:ticket_id>/edit/", staff.edit_ticket, name="edit"),
path("tickets/<int:ticket_id>/update/", path("tickets/<int:ticket_id>/update/", staff.update_ticket_view, name="update"),
staff.update_ticket_view, name="update"), path("tickets/<int:ticket_id>/delete/", staff.delete_ticket, name="delete"),
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>/hold/", staff.hold_ticket, name="hold"),
path("tickets/<int:ticket_id>/unhold/", path("tickets/<int:ticket_id>/unhold/", staff.unhold_ticket, name="unhold"),
staff.unhold_ticket, name="unhold"),
path("tickets/<int:ticket_id>/cc/", staff.ticket_cc, name="ticket_cc"), path("tickets/<int:ticket_id>/cc/", staff.ticket_cc, name="ticket_cc"),
path("tickets/<int:ticket_id>/cc/add/", path("tickets/<int:ticket_id>/cc/add/", staff.ticket_cc_add, name="ticket_cc_add"),
staff.ticket_cc_add, name="ticket_cc_add"),
path( path(
"tickets/<int:ticket_id>/cc/delete/<int:cc_id>/", "tickets/<int:ticket_id>/cc/delete/<int:cc_id>/",
staff.ticket_cc_del, staff.ticket_cc_del,
@ -106,35 +108,33 @@ urlpatterns = [
path( path(
"tickets/<int:ticket_id>/checklists/<int:checklist_id>/", "tickets/<int:ticket_id>/checklists/<int:checklist_id>/",
staff.edit_ticket_checklist, staff.edit_ticket_checklist,
name="edit_ticket_checklist" name="edit_ticket_checklist",
), ),
path( path(
"tickets/<int:ticket_id>/checklists/<int:checklist_id>/delete/", "tickets/<int:ticket_id>/checklists/<int:checklist_id>/delete/",
staff.delete_ticket_checklist, staff.delete_ticket_checklist,
name="delete_ticket_checklist" name="delete_ticket_checklist",
), ),
re_path(r"^raw/(?P<type_>\w+)/$", staff.raw_details, name="raw"), re_path(r"^raw/(?P<type_>\w+)/$", staff.raw_details, name="raw"),
path("rss/", staff.rss_list, name="rss_index"), path("rss/", staff.rss_list, name="rss_index"),
path("reports/", staff.report_index, name="report_index"), path("reports/", staff.report_index, name="report_index"),
re_path(r"^reports/(?P<report>\w+)/$", re_path(r"^reports/(?P<report>\w+)/$", staff.run_report, name="run_report"),
staff.run_report, name="run_report"),
path("save_query/", staff.save_query, name="savequery"), path("save_query/", staff.save_query, name="savequery"),
path("delete_query/<int:pk>/", staff.delete_saved_query, name="delete_query"), path("delete_query/<int:pk>/", staff.delete_saved_query, name="delete_query"),
path("settings/", staff.EditUserSettingsView.as_view(), name="user_settings"), path("settings/", staff.EditUserSettingsView.as_view(), name="user_settings"),
path("ignore/", staff.email_ignore, name="email_ignore"), path("ignore/", staff.email_ignore, name="email_ignore"),
path("ignore/add/", staff.email_ignore_add, name="email_ignore_add"), path("ignore/add/", staff.email_ignore_add, name="email_ignore_add"),
path("ignore/delete/<int:id>/", path("ignore/delete/<int:id>/", staff.email_ignore_del, name="email_ignore_del"),
staff.email_ignore_del, name="email_ignore_del"),
path("checklist-templates/", staff.checklist_templates, name="checklist_templates"), path("checklist-templates/", staff.checklist_templates, name="checklist_templates"),
path( path(
"checklist-templates/<int:checklist_template_id>/", "checklist-templates/<int:checklist_template_id>/",
staff.checklist_templates, staff.checklist_templates,
name="edit_checklist_template" name="edit_checklist_template",
), ),
path( path(
"checklist-templates/<int:checklist_template_id>/delete/", "checklist-templates/<int:checklist_template_id>/delete/",
staff.delete_checklist_template, staff.delete_checklist_template,
name="delete_checklist_template" name="delete_checklist_template",
), ),
re_path( re_path(
r"^datatables_ticket_list/(?P<query>{})$".format(base64_pattern), r"^datatables_ticket_list/(?P<query>{})$".format(base64_pattern),
@ -164,7 +164,11 @@ if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET:
urlpatterns += [ urlpatterns += [
path("", protect_view(public.Homepage.as_view()), name="home"), 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/", public.create_ticket, name="submit"),
path( path(
"tickets/submit_iframe/", "tickets/submit_iframe/",
@ -177,8 +181,7 @@ urlpatterns += [
name="success_iframe", name="success_iframe",
), ),
path("view/", protect_view(public.ViewTicket.as_view()), name="public_view"), path("view/", protect_view(public.ViewTicket.as_view()), name="public_view"),
path("change_language/", public.change_language, path("change_language/", public.change_language, name="public_change_language"),
name="public_change_language"),
] ]
urlpatterns += [ urlpatterns += [
@ -214,8 +217,9 @@ router = DefaultRouter()
router.register(r"tickets", TicketViewSet, basename="ticket") router.register(r"tickets", TicketViewSet, basename="ticket")
router.register(r"user_tickets", UserTicketViewSet, basename="user_tickets") router.register(r"user_tickets", UserTicketViewSet, basename="user_tickets")
router.register(r"followups", FollowUpViewSet, basename="followups") router.register(r"followups", FollowUpViewSet, basename="followups")
router.register(r"followups-attachments", router.register(
FollowUpAttachmentViewSet, basename="followupattachments") r"followups-attachments", FollowUpAttachmentViewSet, basename="followupattachments"
)
router.register(r"users", CreateUserView, basename="user") router.register(r"users", CreateUserView, basename="user")
urlpatterns += [re_path(r"^api/", include(router.urls))] urlpatterns += [re_path(r"^api/", include(router.urls))]
@ -249,8 +253,7 @@ urlpatterns += [
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
urlpatterns += [ urlpatterns += [
path("kb/", kb.index, name="kb_index"), path("kb/", kb.index, name="kb_index"),
re_path(r"^kb/(?P<slug>[A-Za-z0-9_-]+)/$", re_path(r"^kb/(?P<slug>[A-Za-z0-9_-]+)/$", kb.category, name="kb_category"),
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/(?P<item>\d+)/vote/(?P<vote>up|down)/$", kb.vote, name="kb_vote"),
re_path( re_path(
r"^kb_iframe/(?P<slug>[A-Za-z0-9_-]+)/$", r"^kb_iframe/(?P<slug>[A-Za-z0-9_-]+)/$",
@ -268,8 +271,7 @@ urlpatterns += [
path( path(
"system_settings/", "system_settings/",
login_required( login_required(
DirectTemplateView.as_view( DirectTemplateView.as_view(template_name="helpdesk/system_settings.html")
template_name="helpdesk/system_settings.html")
), ),
name="system_settings", name="system_settings",
), ),

View File

@ -1,4 +1,3 @@
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.models import Queue, Ticket from helpdesk.models import Queue, Ticket
@ -23,14 +22,13 @@ class HelpdeskUser:
""" """
user = self.user user = self.user
all_queues = Queue.objects.all() all_queues = Queue.objects.all()
public_ids = [q.pk for q in public_ids = [q.pk for q in Queue.objects.filter(allow_public_submission=True)]
Queue.objects.filter(allow_public_submission=True)] limit_queues_by_user = (
limit_queues_by_user = \ helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION \
and not user.is_superuser and not user.is_superuser
)
if limit_queues_by_user: if limit_queues_by_user:
id_list = [q.pk for q in all_queues if user.has_perm( id_list = [q.pk for q in all_queues if user.has_perm(q.permission_name)]
q.permission_name)]
id_list += public_ids id_list += public_ids
return all_queues.filter(pk__in=id_list) return all_queues.filter(pk__in=id_list)
else: else:
@ -56,8 +54,11 @@ class HelpdeskUser:
return Ticket.objects.filter(queue__in=self.get_queues()) return Ticket.objects.filter(queue__in=self.get_queues())
def has_full_access(self): 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 or helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
)
def can_access_queue(self, queue): def can_access_queue(self, queue):
"""Check if a certain user can access a certain queue. """Check if a certain user can access a certain queue.
@ -71,18 +72,18 @@ class HelpdeskUser:
else: else:
return ( return (
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION
and and self.user.has_perm(queue.permission_name)
self.user.has_perm(queue.permission_name)
) )
def can_access_ticket(self, ticket): def can_access_ticket(self, ticket):
"""Check to see if the user has permission to access """Check to see if the user has permission to access
a ticket. If not then deny access.""" a ticket. If not then deny access."""
user = self.user user = self.user
if self.can_access_queue(ticket.queue): if self.can_access_queue(ticket.queue):
return True return True
elif self.has_full_access() or \ elif self.has_full_access() or (
(ticket.assigned_to and user.id == ticket.assigned_to.id): ticket.assigned_to and user.id == ticket.assigned_to.id
):
return True return True
else: else:
return False return False
@ -90,4 +91,6 @@ class HelpdeskUser:
def can_access_kbcategory(self, category): def can_access_kbcategory(self, category):
if category.public: if category.public:
return True 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)
)

View File

@ -14,6 +14,7 @@ from helpdesk import settings as helpdesk_settings
def validate_file_extension(value): def validate_file_extension(value):
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
import os import os
ext = os.path.splitext(value.name)[1] # [0] returns path+filename ext = os.path.splitext(value.name)[1] # [0] returns path+filename
# TODO: we might improve this with more thorough checks of file types # TODO: we might improve this with more thorough checks of file types
# rather than just the extensions. # rather than just the extensions.
@ -24,7 +25,5 @@ def validate_file_extension(value):
if ext.lower() not in helpdesk_settings.HELPDESK_VALID_EXTENSIONS: if ext.lower() not in helpdesk_settings.HELPDESK_VALID_EXTENSIONS:
# TODO: one more check in case it is a file with no extension; we # TODO: one more check in case it is a file with no extension; we
# should always allow that? # should always allow that?
if not (ext.lower() == '' or ext.lower() == '.'): if not (ext.lower() == "" or ext.lower() == "."):
raise ValidationError( raise ValidationError(_("Unsupported file extension: ") + ext.lower())
_('Unsupported file extension: ') + ext.lower()
)

View File

@ -1,23 +1,28 @@
from helpdesk.models import CustomField, KBItem, Queue from helpdesk.models import CustomField, KBItem, Queue
class AbstractCreateTicketMixin(): class AbstractCreateTicketMixin:
def get_initial(self): def get_initial(self):
initial_data = {} initial_data = {}
request = self.request request = self.request
try: try:
initial_data['queue'] = Queue.objects.get( initial_data["queue"] = Queue.objects.get(
slug=request.GET.get('queue', None)).id slug=request.GET.get("queue", None)
).id
except Queue.DoesNotExist: except Queue.DoesNotExist:
pass pass
u = request.user u = request.user
if u.is_authenticated and u.usersettings_helpdesk.use_email_as_submitter and u.email: if (
initial_data['submitter_email'] = u.email 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', query_param_fields = ["submitter_email", "title", "body", "queue", "kbitem"]
'title', 'body', 'queue', 'kbitem']
custom_fields = [ 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 query_param_fields += custom_fields
for qpf in query_param_fields: for qpf in query_param_fields:
initial_data[qpf] = request.GET.get(qpf, initial_data.get(qpf, "")) initial_data[qpf] = request.GET.get(qpf, initial_data.get(qpf, ""))
@ -27,13 +32,12 @@ class AbstractCreateTicketMixin():
def get_form_kwargs(self, *args, **kwargs): def get_form_kwargs(self, *args, **kwargs):
kwargs = super().get_form_kwargs(*args, **kwargs) kwargs = super().get_form_kwargs(*args, **kwargs)
kbitem = self.request.GET.get( kbitem = self.request.GET.get(
'kbitem', "kbitem",
self.request.POST.get('kbitem', None), self.request.POST.get("kbitem", None),
) )
if kbitem: if kbitem:
try: try:
kwargs['kbcategory'] = KBItem.objects.get( kwargs["kbcategory"] = KBItem.objects.get(pk=int(kbitem)).category
pk=int(kbitem)).category
except (ValueError, KBItem.DoesNotExist): except (ValueError, KBItem.DoesNotExist):
pass pass
return kwargs return kwargs

View File

@ -1,6 +1,12 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from helpdesk.models import FollowUp, FollowUpAttachment, Ticket 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 import viewsets
from rest_framework.mixins import CreateModelMixin from rest_framework.mixins import CreateModelMixin
from rest_framework.permissions import IsAdminUser, IsAuthenticated from rest_framework.permissions import IsAdminUser, IsAuthenticated
@ -12,7 +18,7 @@ from helpdesk import settings as helpdesk_settings
class ConservativePagination(PageNumberPagination): class ConservativePagination(PageNumberPagination):
page_size = 25 page_size = 25
page_size_query_param = 'page_size' page_size_query_param = "page_size"
class UserTicketViewSet(viewsets.ReadOnlyModelViewSet): class UserTicketViewSet(viewsets.ReadOnlyModelViewSet):
@ -21,18 +27,20 @@ class UserTicketViewSet(viewsets.ReadOnlyModelViewSet):
The view is paginated by default The view is paginated by default
""" """
serializer_class = PublicTicketListingSerializer serializer_class = PublicTicketListingSerializer
pagination_class = ConservativePagination pagination_class = ConservativePagination
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def get_queryset(self): 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: for ticket in tickets:
ticket.set_custom_field_values() ticket.set_custom_field_values()
return tickets return tickets
class TicketViewSet(viewsets.ModelViewSet): class TicketViewSet(viewsets.ModelViewSet):
""" """
A viewset that provides the standard actions to handle Ticket 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. `/api/tickets/?status=Open,Resolved` will return all the tickets that are Open or Resolved.
""" """
queryset = Ticket.objects.all() queryset = Ticket.objects.all()
serializer_class = TicketSerializer serializer_class = TicketSerializer
pagination_class = ConservativePagination pagination_class = ConservativePagination
@ -50,17 +59,17 @@ class TicketViewSet(viewsets.ModelViewSet):
tickets = Ticket.objects.all() tickets = Ticket.objects.all()
# filter by status # filter by status
status = self.request.query_params.get('status', None) status = self.request.query_params.get("status", None)
if status: if status:
statuses = status.split(',') if status else [] statuses = status.split(",") if status else []
status_choices = helpdesk_settings.TICKET_STATUS_CHOICES status_choices = helpdesk_settings.TICKET_STATUS_CHOICES
number_statuses = [] number_statuses = []
for status in statuses: for status in statuses:
for choice in status_choices: for choice in status_choices:
if str(choice[0]) == status: if str(choice[0]) == status:
number_statuses.append(choice[0]) number_statuses.append(choice[0])
if number_statuses: if number_statuses:
tickets = tickets.filter(status__in=number_statuses) tickets = tickets.filter(status__in=number_statuses)
for ticket in tickets: for ticket in tickets:
ticket.set_custom_field_values() ticket.set_custom_field_values()

View File

@ -25,8 +25,8 @@ for open_status in Ticket.OPEN_STATUSES:
class OpenTicketsByUser(Feed): class OpenTicketsByUser(Feed):
title_template = 'helpdesk/rss/ticket_title.html' title_template = "helpdesk/rss/ticket_title.html"
description_template = 'helpdesk/rss/ticket_description.html' description_template = "helpdesk/rss/ticket_description.html"
def get_object(self, request, user_name, queue_slug=None): def get_object(self, request, user_name, queue_slug=None):
user = get_object_or_404(User, username=user_name) user = get_object_or_404(User, username=user_name)
@ -35,54 +35,56 @@ class OpenTicketsByUser(Feed):
else: else:
queue = None queue = None
return {'user': user, 'queue': queue} return {"user": user, "queue": queue}
def title(self, obj): def title(self, obj):
if obj['queue']: if obj["queue"]:
return _("Helpdesk: Open Tickets in queue %(queue)s for %(username)s") % { return _("Helpdesk: Open Tickets in queue %(queue)s for %(username)s") % {
'queue': obj['queue'].title, "queue": obj["queue"].title,
'username': obj['user'].get_username(), "username": obj["user"].get_username(),
} }
else: else:
return _("Helpdesk: Open Tickets for %(username)s") % { return _("Helpdesk: Open Tickets for %(username)s") % {
'username': obj['user'].get_username(), "username": obj["user"].get_username(),
} }
def description(self, obj): def description(self, obj):
if obj['queue']: if obj["queue"]:
return _("Open and Reopened Tickets in queue %(queue)s for %(username)s") % { return _(
'queue': obj['queue'].title, "Open and Reopened Tickets in queue %(queue)s for %(username)s"
'username': obj['user'].get_username(), ) % {
"queue": obj["queue"].title,
"username": obj["user"].get_username(),
} }
else: else:
return _("Open and Reopened Tickets for %(username)s") % { return _("Open and Reopened Tickets for %(username)s") % {
'username': obj['user'].get_username(), "username": obj["user"].get_username(),
} }
def link(self, obj): def link(self, obj):
if obj['queue']: if obj["queue"]:
return u'%s?assigned_to=%s&queue=%s' % ( return "%s?assigned_to=%s&queue=%s" % (
reverse('helpdesk:list'), reverse("helpdesk:list"),
obj['user'].id, obj["user"].id,
obj['queue'].id, obj["queue"].id,
) )
else: else:
return u'%s?assigned_to=%s' % ( return "%s?assigned_to=%s" % (
reverse('helpdesk:list'), reverse("helpdesk:list"),
obj['user'].id, obj["user"].id,
) )
def items(self, obj): def items(self, obj):
if obj['queue']: if obj["queue"]:
return Ticket.objects.filter( return (
assigned_to=obj['user'] Ticket.objects.filter(assigned_to=obj["user"])
).filter( .filter(queue=obj["queue"])
queue=obj['queue'] .filter(Q_OPEN_STATUSES)
).filter(Q_OPEN_STATUSES) )
else: else:
return Ticket.objects.filter( return Ticket.objects.filter(assigned_to=obj["user"]).filter(
assigned_to=obj['user'] Q_OPEN_STATUSES
).filter(Q_OPEN_STATUSES) )
def item_pubdate(self, item): def item_pubdate(self, item):
return item.created return item.created
@ -91,21 +93,19 @@ class OpenTicketsByUser(Feed):
if item.assigned_to: if item.assigned_to:
return item.assigned_to.get_username() return item.assigned_to.get_username()
else: else:
return _('Unassigned') return _("Unassigned")
class UnassignedTickets(Feed): class UnassignedTickets(Feed):
title_template = 'helpdesk/rss/ticket_title.html' title_template = "helpdesk/rss/ticket_title.html"
description_template = 'helpdesk/rss/ticket_description.html' description_template = "helpdesk/rss/ticket_description.html"
title = _('Helpdesk: Unassigned Tickets') title = _("Helpdesk: Unassigned Tickets")
description = _('Unassigned Open and Reopened tickets') description = _("Unassigned Open and Reopened tickets")
link = '' # '%s?assigned_to=' % reverse('helpdesk:list') link = "" # '%s?assigned_to=' % reverse('helpdesk:list')
def items(self, obj): def items(self, obj):
return Ticket.objects.filter( return Ticket.objects.filter(assigned_to__isnull=True).filter(Q_OPEN_STATUSES)
assigned_to__isnull=True
).filter(Q_OPEN_STATUSES)
def item_pubdate(self, item): def item_pubdate(self, item):
return item.created return item.created
@ -114,49 +114,48 @@ class UnassignedTickets(Feed):
if item.assigned_to: if item.assigned_to:
return item.assigned_to.get_username() return item.assigned_to.get_username()
else: else:
return _('Unassigned') return _("Unassigned")
class RecentFollowUps(Feed): class RecentFollowUps(Feed):
title_template = 'helpdesk/rss/recent_activity_title.html' title_template = "helpdesk/rss/recent_activity_title.html"
description_template = 'helpdesk/rss/recent_activity_description.html' description_template = "helpdesk/rss/recent_activity_description.html"
title = _('Helpdesk: Recent Followups') title = _("Helpdesk: Recent Followups")
description = _( description = _(
'Recent FollowUps, such as e-mail replies, comments, attachments and resolutions') "Recent FollowUps, such as e-mail replies, comments, attachments and resolutions"
link = '/tickets/' # reverse('helpdesk:list') )
link = "/tickets/" # reverse('helpdesk:list')
def items(self): def items(self):
return FollowUp.objects.order_by('-date')[:20] return FollowUp.objects.order_by("-date")[:20]
class OpenTicketsByQueue(Feed): class OpenTicketsByQueue(Feed):
title_template = 'helpdesk/rss/ticket_title.html' title_template = "helpdesk/rss/ticket_title.html"
description_template = 'helpdesk/rss/ticket_description.html' description_template = "helpdesk/rss/ticket_description.html"
def get_object(self, request, queue_slug): def get_object(self, request, queue_slug):
return get_object_or_404(Queue, slug=queue_slug) return get_object_or_404(Queue, slug=queue_slug)
def title(self, obj): def title(self, obj):
return _('Helpdesk: Open Tickets in queue %(queue)s') % { return _("Helpdesk: Open Tickets in queue %(queue)s") % {
'queue': obj.title, "queue": obj.title,
} }
def description(self, obj): def description(self, obj):
return _('Open and Reopened Tickets in queue %(queue)s') % { return _("Open and Reopened Tickets in queue %(queue)s") % {
'queue': obj.title, "queue": obj.title,
} }
def link(self, obj): def link(self, obj):
return '%s?queue=%s' % ( return "%s?queue=%s" % (
reverse('helpdesk:list'), reverse("helpdesk:list"),
obj.id, obj.id,
) )
def items(self, obj): def items(self, obj):
return Ticket.objects.filter( return Ticket.objects.filter(queue=obj).filter(Q_OPEN_STATUSES)
queue=obj
).filter(Q_OPEN_STATUSES)
def item_pubdate(self, item): def item_pubdate(self, item):
return item.created return item.created
@ -165,4 +164,4 @@ class OpenTicketsByQueue(Feed):
if item.assigned_to: if item.assigned_to:
return item.assigned_to.get_username() return item.assigned_to.get_username()
else: else:
return _('Unassigned') return _("Unassigned")

View File

@ -18,10 +18,14 @@ from helpdesk.models import KBCategory, KBItem
def index(request): def index(request):
huser = user.huser_from_request(request) huser = user.huser_from_request(request)
# TODO: It'd be great to have a list of most popular items here. # TODO: It'd be great to have a list of most popular items here.
return render(request, 'helpdesk/kb_index.html', { return render(
'kb_categories': huser.get_allowed_kb_categories(), request,
'helpdesk_settings': helpdesk_settings, "helpdesk/kb_index.html",
}) {
"kb_categories": huser.get_allowed_kb_categories(),
"helpdesk_settings": helpdesk_settings,
},
)
def category(request, slug, iframe=False): 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): if not user.huser_from_request(request).can_access_kbcategory(category):
raise Http404 raise Http404
items = category.kbitem_set.filter(enabled=True) items = category.kbitem_set.filter(enabled=True)
selected_item = request.GET.get('kbitem', None) selected_item = request.GET.get("kbitem", None)
try: try:
selected_item = int(selected_item) selected_item = int(selected_item)
except TypeError: except TypeError:
pass pass
qparams = request.GET.copy() qparams = request.GET.copy()
try: try:
del qparams['kbitem'] del qparams["kbitem"]
except KeyError: except KeyError:
pass pass
template = 'helpdesk/kb_category.html' template = "helpdesk/kb_category.html"
if iframe: if iframe:
template = 'helpdesk/kb_category_iframe.html' template = "helpdesk/kb_category_iframe.html"
staff = request.user.is_authenticated and request.user.is_staff staff = request.user.is_authenticated and request.user.is_staff
return render(request, template, { return render(
'category': category, request,
'items': items, template,
'selected_item': selected_item, {
'query_param_string': qparams.urlencode(), "category": category,
'helpdesk_settings': helpdesk_settings, "items": items,
'iframe': iframe, "selected_item": selected_item,
'staff': staff, "query_param_string": qparams.urlencode(),
}) "helpdesk_settings": helpdesk_settings,
"iframe": iframe,
"staff": staff,
},
)
@xframe_options_exempt @xframe_options_exempt
@ -62,7 +70,7 @@ def category_iframe(request, slug):
def vote(request, item, vote): def vote(request, item, vote):
item = get_object_or_404(KBItem, pk=item) item = get_object_or_404(KBItem, pk=item)
if request.method == "POST": if request.method == "POST":
if vote == 'up': if vote == "up":
if not item.voted_by.filter(pk=request.user.pk): if not item.voted_by.filter(pk=request.user.pk):
item.votes += 1 item.votes += 1
item.voted_by.add(request.user.pk) 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): if item.downvoted_by.filter(pk=request.user.pk):
item.votes -= 1 item.votes -= 1
item.downvoted_by.remove(request.user.pk) item.downvoted_by.remove(request.user.pk)
if vote == 'down': if vote == "down":
if not item.downvoted_by.filter(pk=request.user.pk): if not item.downvoted_by.filter(pk=request.user.pk):
item.votes += 1 item.votes += 1
item.downvoted_by.add(request.user.pk) item.downvoted_by.add(request.user.pk)

View File

@ -5,24 +5,22 @@ from django.shortcuts import resolve_url
default_login_view = auth_views.LoginView.as_view( default_login_view = auth_views.LoginView.as_view(
template_name='helpdesk/registration/login.html') template_name="helpdesk/registration/login.html"
)
def login(request): def login(request):
login_url = settings.LOGIN_URL login_url = settings.LOGIN_URL
# Prevent redirect loop by checking that LOGIN_URL is not this view's name # Prevent redirect loop by checking that LOGIN_URL is not this view's name
condition = ( condition = login_url and (
login_url login_url != resolve_url(request.resolver_match.view_name)
and ( and (login_url != request.resolver_match.view_name)
login_url != resolve_url(request.resolver_match.view_name)
and (login_url != request.resolver_match.view_name)
)
) )
if condition: if condition:
if 'next' in request.GET: if "next" in request.GET:
return_to = request.GET['next'] return_to = request.GET["next"]
else: else:
return_to = resolve_url('helpdesk:home') return_to = resolve_url("helpdesk:home")
return redirect_to_login(return_to, login_url) return redirect_to_login(return_to, login_url)
else: else:
return default_login_view(request) return default_login_view(request)

View File

@ -7,9 +7,12 @@ views/public.py - All public facing views, eg non-staff (no authentication
required) views. required) views.
""" """
from django.conf import settings 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.http import HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
@ -41,11 +44,11 @@ def create_ticket(request, *args, **kwargs):
class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView): class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
def get_form_class(self): def get_form_class(self):
try: try:
the_module, the_form_class = helpdesk_settings.HELPDESK_PUBLIC_TICKET_FORM_CLASS.rsplit( the_module, the_form_class = (
".", 1) helpdesk_settings.HELPDESK_PUBLIC_TICKET_FORM_CLASS.rsplit(".", 1)
)
the_module = import_module(the_module) the_module = import_module(the_module)
the_form_class = getattr(the_module, the_form_class) the_form_class = getattr(the_module, the_form_class)
except Exception as e: except Exception as e:
@ -56,76 +59,85 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
request = self.request request = self.request
if not request.user.is_authenticated and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT: if (
return HttpResponseRedirect(reverse('login')) 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 \ if is_helpdesk_staff(request.user) or (
(request.user.is_authenticated and request.user.is_authenticated
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE): and helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
):
try: try:
if request.user.usersettings_helpdesk.login_view_ticketlist: if request.user.usersettings_helpdesk.login_view_ticketlist:
return HttpResponseRedirect(reverse('helpdesk:list')) return HttpResponseRedirect(reverse("helpdesk:list"))
else: else:
return HttpResponseRedirect(reverse('helpdesk:dashboard')) return HttpResponseRedirect(reverse("helpdesk:dashboard"))
except UserSettings.DoesNotExist: except UserSettings.DoesNotExist:
return HttpResponseRedirect(reverse('helpdesk:dashboard')) return HttpResponseRedirect(reverse("helpdesk:dashboard"))
return super().dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
def get_initial(self): def get_initial(self):
initial_data = super().get_initial() initial_data = super().get_initial()
# add pre-defined data for public ticket # add pre-defined data for public ticket
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE'): if hasattr(settings, "HELPDESK_PUBLIC_TICKET_QUEUE"):
# get the requested queue; return an error if queue not found # get the requested queue; return an error if queue not found
try: try:
initial_data['queue'] = Queue.objects.get( initial_data["queue"] = Queue.objects.get(
slug=settings.HELPDESK_PUBLIC_TICKET_QUEUE, slug=settings.HELPDESK_PUBLIC_TICKET_QUEUE,
allow_public_submission=True allow_public_submission=True,
).id ).id
except Queue.DoesNotExist as e: except Queue.DoesNotExist as e:
logger.fatal( logger.fatal(
"Public queue '%s' is configured as default but can't be found", "Public queue '%s' is configured as default but can't be found",
settings.HELPDESK_PUBLIC_TICKET_QUEUE settings.HELPDESK_PUBLIC_TICKET_QUEUE,
) )
raise ImproperlyConfigured( raise ImproperlyConfigured("Wrong public queue configuration") from e
"Wrong public queue configuration") from e if hasattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY"):
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'): initial_data["priority"] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY
initial_data['priority'] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY if hasattr(settings, "HELPDESK_PUBLIC_TICKET_DUE_DATE"):
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_DUE_DATE'): initial_data["due_date"] = settings.HELPDESK_PUBLIC_TICKET_DUE_DATE
initial_data['due_date'] = settings.HELPDESK_PUBLIC_TICKET_DUE_DATE
return initial_data return initial_data
def get_form_kwargs(self, *args, **kwargs): def get_form_kwargs(self, *args, **kwargs):
kwargs = super().get_form_kwargs(*args, **kwargs) kwargs = super().get_form_kwargs(*args, **kwargs)
if '_hide_fields_' in self.request.GET: if "_hide_fields_" in self.request.GET:
kwargs['hidden_fields'] = self.request.GET.get( kwargs["hidden_fields"] = self.request.GET.get("_hide_fields_", "").split(
'_hide_fields_', '').split(',') ","
kwargs['readonly_fields'] = self.request.GET.get( )
'_readonly_fields_', '').split(',') kwargs["readonly_fields"] = self.request.GET.get("_readonly_fields_", "").split(
","
)
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
request = self.request 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. # 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: else:
ticket = form.save( 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: try:
return HttpResponseRedirect('%s?ticket=%s&email=%s&key=%s' % ( return HttpResponseRedirect(
reverse('helpdesk:public_view'), "%s?ticket=%s&email=%s&key=%s"
ticket.ticket_for_url, % (
quote(ticket.submitter_email), reverse("helpdesk:public_view"),
ticket.secret_key) ticket.ticket_for_url,
quote(ticket.submitter_email),
ticket.secret_key,
)
) )
except ValueError: except ValueError:
# if someone enters a non-int string for the ticket # if someone enters a non-int string for the ticket
return HttpResponseRedirect(reverse('helpdesk:home')) return HttpResponseRedirect(reverse("helpdesk:home"))
class CreateTicketIframeView(BaseCreateTicketView): class CreateTicketIframeView(BaseCreateTicketView):
template_name = 'helpdesk/public_create_ticket_iframe.html' template_name = "helpdesk/public_create_ticket_iframe.html"
@csrf_exempt @csrf_exempt
@xframe_options_exempt @xframe_options_exempt
@ -134,11 +146,11 @@ class CreateTicketIframeView(BaseCreateTicketView):
def form_valid(self, form): def form_valid(self, form):
if super().form_valid(form).status_code == 302: if super().form_valid(form).status_code == 302:
return HttpResponseRedirect(reverse('helpdesk:success_iframe')) return HttpResponseRedirect(reverse("helpdesk:success_iframe"))
class SuccessIframeView(TemplateView): class SuccessIframeView(TemplateView):
template_name = 'helpdesk/success_iframe.html' template_name = "helpdesk/success_iframe.html"
@xframe_options_exempt @xframe_options_exempt
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
@ -146,123 +158,140 @@ class SuccessIframeView(TemplateView):
class CreateTicketView(BaseCreateTicketView): class CreateTicketView(BaseCreateTicketView):
template_name = 'helpdesk/public_create_ticket.html' template_name = "helpdesk/public_create_ticket.html"
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
# Add the CSS error class to the form in order to better see them in # Add the CSS error class to the form in order to better see them in
# the page # the page
form.error_css_class = 'text-danger' form.error_css_class = "text-danger"
return form return form
class Homepage(CreateTicketView): class Homepage(CreateTicketView):
template_name = 'helpdesk/public_homepage.html' template_name = "helpdesk/public_homepage.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['kb_categories'] = huser_from_request( context["kb_categories"] = huser_from_request(
self.request).get_allowed_kb_categories() self.request
).get_allowed_kb_categories()
return context return context
class SearchForTicketView(TemplateView): class SearchForTicketView(TemplateView):
template_name = 'helpdesk/public_view_form.html' template_name = "helpdesk/public_view_form.html"
def get(self, request, *args, **kwargs): 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) context = self.get_context_data(**kwargs)
return self.render_to_response(context) return self.render_to_response(context)
else: 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): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
request = self.request request = self.request
email = request.GET.get('email', None) email = request.GET.get("email", None)
error_message = kwargs.get('error_message', None) error_message = kwargs.get("error_message", None)
context.update({ context.update(
'ticket': False, {
'email': email, "ticket": False,
'error_message': error_message, "email": email,
'helpdesk_settings': helpdesk_settings, "error_message": error_message,
}) "helpdesk_settings": helpdesk_settings,
}
)
return context return context
class ViewTicket(TemplateView): class ViewTicket(TemplateView):
template_name = 'helpdesk/public_view_ticket.html' template_name = "helpdesk/public_view_ticket.html"
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
ticket_req = request.GET.get('ticket', None) ticket_req = request.GET.get("ticket", None)
email = request.GET.get('email', None) email = request.GET.get("email", None)
key = request.GET.get('key', '') key = request.GET.get("key", "")
if not (ticket_req and email): if not (ticket_req and email):
if ticket_req is None and email is None: if ticket_req is None and email is None:
return SearchForTicketView.as_view()(request) return SearchForTicketView.as_view()(request)
else: 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: try:
queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req) queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req)
if request.user.is_authenticated and request.user.email == email: if request.user.is_authenticated and request.user.email == email:
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=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) ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email)
else: 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): 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 from helpdesk.update_ticket import update_ticket
update_ticket( update_ticket(
request.user, request.user,
ticket, ticket,
public=True, public=True,
comment=_('Submitter accepted resolution and closed ticket'), comment=_("Submitter accepted resolution and closed ticket"),
new_status=Ticket.CLOSED_STATUS, new_status=Ticket.CLOSED_STATUS,
) )
return HttpResponseRedirect(ticket.ticket_url) return HttpResponseRedirect(ticket.ticket_url)
# Prepare context for rendering # Prepare context for rendering
context = { context = {
'key': key, "key": key,
'mail': email, "mail": email,
'ticket': ticket, "ticket": ticket,
'helpdesk_settings': helpdesk_settings, "helpdesk_settings": helpdesk_settings,
'next': self.get_next_url(ticket_id) "next": self.get_next_url(ticket_id),
} }
return self.render_to_response(context) return self.render_to_response(context)
def get_next_url(self, ticket_id): def get_next_url(self, ticket_id):
redirect_url = '' redirect_url = ""
if is_helpdesk_staff(self.request.user): if is_helpdesk_staff(self.request.user):
redirect_url = reverse('helpdesk:view', args=[ticket_id]) redirect_url = reverse("helpdesk:view", args=[ticket_id])
if 'close' in self.request.GET: if "close" in self.request.GET:
redirect_url += '?close' redirect_url += "?close"
elif helpdesk_settings.HELPDESK_NAVIGATION_ENABLED: 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 return redirect_url
class MyTickets(TemplateView): class MyTickets(TemplateView):
template_name = 'helpdesk/my_tickets.html' template_name = "helpdesk/my_tickets.html"
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if not request.user.is_authenticated: if not request.user.is_authenticated:
return HttpResponseRedirect(reverse('helpdesk:login')) return HttpResponseRedirect(reverse("helpdesk:login"))
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
return self.render_to_response(context) return self.render_to_response(context)
def change_language(request): def change_language(request):
return_to = '' return_to = ""
if 'return_to' in request.GET: if "return_to" in request.GET:
return_to = request.GET['return_to'] 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

View File

@ -8,6 +8,7 @@ from .signals import new_ticket_done, update_ticket_done
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def notify_followup_webhooks(followup): def notify_followup_webhooks(followup):
urls = settings.HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS() urls = settings.HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS()
if not urls: if not urls:
@ -15,22 +16,24 @@ def notify_followup_webhooks(followup):
# Serialize the ticket associated with the followup # Serialize the ticket associated with the followup
from .serializers import TicketSerializer from .serializers import TicketSerializer
ticket = followup.ticket ticket = followup.ticket
ticket.set_custom_field_values() ticket.set_custom_field_values()
serialized_ticket = TicketSerializer(ticket).data serialized_ticket = TicketSerializer(ticket).data
# Prepare the data to send # Prepare the data to send
data = { data = {
'ticket': serialized_ticket, "ticket": serialized_ticket,
'queue_slug': ticket.queue.slug, "queue_slug": ticket.queue.slug,
'followup_id': followup.id "followup_id": followup.id,
} }
for url in urls: for url in urls:
try: try:
requests.post(url, json=data, timeout=settings.HELPDESK_WEBHOOK_TIMEOUT) requests.post(url, json=data, timeout=settings.HELPDESK_WEBHOOK_TIMEOUT)
except requests.exceptions.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() # listener is loaded via app.py HelpdeskConfig.ready()
@receiver(update_ticket_done) @receiver(update_ticket_done)
@ -44,22 +47,21 @@ def send_new_ticket_webhook(ticket):
return return
# Serialize the ticket # Serialize the ticket
from .serializers import TicketSerializer from .serializers import TicketSerializer
ticket.set_custom_field_values() ticket.set_custom_field_values()
serialized_ticket = TicketSerializer(ticket).data serialized_ticket = TicketSerializer(ticket).data
# Prepare the data to send # Prepare the data to send
data = { data = {"ticket": serialized_ticket, "queue_slug": ticket.queue.slug}
'ticket': serialized_ticket,
'queue_slug': ticket.queue.slug
}
for url in urls: for url in urls:
try: try:
requests.post(url, json=data, timeout=settings.HELPDESK_WEBHOOK_TIMEOUT) requests.post(url, json=data, timeout=settings.HELPDESK_WEBHOOK_TIMEOUT)
except requests.exceptions.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() # listener is loaded via app.py HelpdeskConfig.ready()
@receiver(new_ticket_done) @receiver(new_ticket_done)
def send_new_ticket_webhook_receiver(sender, ticket, **kwargs): 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