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

View File

@ -1,5 +1,5 @@
Copyright (c) 2008 Ross Poulton (Trading as Jutda),
Copyright (c) 2008-2021 django-helpdesk contributors.
Copyright (c) 2008-2025 django-helpdesk contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,

View File

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

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
:target: https://codecov.io/gh/django-helpdesk/django-helpdesk
Copyright 2009-2023 Ross Poulton and django-helpdesk contributors. All Rights Reserved.
Copyright 2009-2025 Ross Poulton and django-helpdesk contributors. All Rights Reserved.
See LICENSE for details.
django-helpdesk was formerly known as Jutda Helpdesk, named after the

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -4,23 +4,42 @@ from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('helpdesk', '0003_initial_data_import'),
("helpdesk", "0003_initial_data_import"),
]
operations = [
migrations.CreateModel(
name='QueueMembership',
name="QueueMembership",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('queues', models.ManyToManyField(to='helpdesk.Queue', verbose_name='Authorized Queues')),
('user', models.OneToOneField(verbose_name='User', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"queues",
models.ManyToManyField(
to="helpdesk.Queue", verbose_name="Authorized Queues"
),
),
(
"user",
models.OneToOneField(
verbose_name="User",
to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
),
),
],
options={
'verbose_name': 'Queue Membership',
'verbose_name_plural': 'Queue Memberships',
"verbose_name": "Queue Membership",
"verbose_name_plural": "Queue Memberships",
},
bases=(models.Model,),
),

View File

@ -3,25 +3,36 @@ from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0004_add_per_queue_staff_membership'),
("helpdesk", "0004_add_per_queue_staff_membership"),
]
operations = [
migrations.AlterField(
model_name='escalationexclusion',
name='queues',
field=models.ManyToManyField(help_text='Leave blank for this exclusion to be applied to all queues, or select those queues you wish to exclude with this entry.', to='helpdesk.Queue', blank=True),
model_name="escalationexclusion",
name="queues",
field=models.ManyToManyField(
help_text="Leave blank for this exclusion to be applied to all queues, or select those queues you wish to exclude with this entry.",
to="helpdesk.Queue",
blank=True,
),
),
migrations.AlterField(
model_name='ignoreemail',
name='queues',
field=models.ManyToManyField(help_text='Leave blank for this e-mail to be ignored on all queues, or select those queues you wish to ignore this e-mail for.', to='helpdesk.Queue', blank=True),
model_name="ignoreemail",
name="queues",
field=models.ManyToManyField(
help_text="Leave blank for this e-mail to be ignored on all queues, or select those queues you wish to ignore this e-mail for.",
to="helpdesk.Queue",
blank=True,
),
),
migrations.AlterField(
model_name='presetreply',
name='queues',
field=models.ManyToManyField(help_text='Leave blank to allow this reply to be used for all queues, or select those queues you wish to limit this reply to.', to='helpdesk.Queue', blank=True),
model_name="presetreply",
name="queues",
field=models.ManyToManyField(
help_text="Leave blank to allow this reply to be used for all queues, or select those queues you wish to limit this reply to.",
to="helpdesk.Queue",
blank=True,
),
),
]

View File

@ -3,25 +3,42 @@ from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0005_queues_no_null'),
("helpdesk", "0005_queues_no_null"),
]
operations = [
migrations.AlterField(
model_name='queue',
name='email_address',
field=models.EmailField(help_text='All outgoing e-mails for this queue will use this e-mail address. If you use IMAP or POP3, this should be the e-mail address for that mailbox.', max_length=254, null=True, verbose_name='E-Mail Address', blank=True),
model_name="queue",
name="email_address",
field=models.EmailField(
help_text="All outgoing e-mails for this queue will use this e-mail address. If you use IMAP or POP3, this should be the e-mail address for that mailbox.",
max_length=254,
null=True,
verbose_name="E-Mail Address",
blank=True,
),
),
migrations.AlterField(
model_name='ticket',
name='submitter_email',
field=models.EmailField(help_text='The submitter will receive an email for all public follow-ups left for this task.', max_length=254, null=True, verbose_name='Submitter E-Mail', blank=True),
model_name="ticket",
name="submitter_email",
field=models.EmailField(
help_text="The submitter will receive an email for all public follow-ups left for this task.",
max_length=254,
null=True,
verbose_name="Submitter E-Mail",
blank=True,
),
),
migrations.AlterField(
model_name='ticketcc',
name='email',
field=models.EmailField(help_text='For non-user followers, enter their e-mail address', max_length=254, null=True, verbose_name='E-Mail Address', blank=True),
model_name="ticketcc",
name="email",
field=models.EmailField(
help_text="For non-user followers, enter their e-mail address",
max_length=254,
null=True,
verbose_name="E-Mail Address",
blank=True,
),
),
]

View File

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

View File

@ -3,15 +3,20 @@ from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0007_max_length_by_integer'),
("helpdesk", "0007_max_length_by_integer"),
]
operations = [
migrations.AddField(
model_name='queue',
name='permission_name',
field=models.CharField(help_text='Name used in the django.contrib.auth permission system', max_length=50, null=True, verbose_name='Django auth permission name', blank=True),
model_name="queue",
name="permission_name",
field=models.CharField(
help_text="Name used in the django.contrib.auth permission system",
max_length=50,
null=True,
verbose_name="Django auth permission name",
blank=True,
),
),
]

View File

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

View File

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

View File

@ -3,20 +3,30 @@ from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0010_remove_queuemembership'),
("helpdesk", "0010_remove_queuemembership"),
]
operations = [
migrations.AlterField(
model_name='queue',
name='permission_name',
field=models.CharField(editable=False, max_length=50, blank=True, help_text='Name used in the django.contrib.auth permission system', null=True, verbose_name='Django auth permission name'),
model_name="queue",
name="permission_name",
field=models.CharField(
editable=False,
max_length=50,
blank=True,
help_text="Name used in the django.contrib.auth permission system",
null=True,
verbose_name="Django auth permission name",
),
),
migrations.AlterField(
model_name='queue',
name='slug',
field=models.SlugField(help_text="This slug is used when building ticket ID's. Once set, try not to change it or e-mailing may get messy.", unique=True, verbose_name='Slug'),
model_name="queue",
name="slug",
field=models.SlugField(
help_text="This slug is used when building ticket ID's. Once set, try not to change it or e-mailing may get messy.",
unique=True,
verbose_name="Slug",
),
),
]

View File

@ -6,16 +6,22 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('helpdesk', '0011_admin_related_improvements'),
("helpdesk", "0011_admin_related_improvements"),
]
operations = [
migrations.AddField(
model_name='queue',
name='default_owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='default_owner', to=settings.AUTH_USER_MODEL, verbose_name='Default owner'),
model_name="queue",
name="default_owner",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="default_owner",
to=settings.AUTH_USER_MODEL,
verbose_name="Default owner",
),
),
]

View File

@ -4,30 +4,66 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0012_queue_default_owner'),
("helpdesk", "0012_queue_default_owner"),
]
operations = [
migrations.AddField(
model_name='queue',
name='email_box_local_dir',
field=models.CharField(blank=True, help_text='If using a local directory, what directory path do you wish to poll for new email? Example: /var/lib/mail/helpdesk/', max_length=200, null=True, verbose_name='E-Mail Local Directory'),
model_name="queue",
name="email_box_local_dir",
field=models.CharField(
blank=True,
help_text="If using a local directory, what directory path do you wish to poll for new email? Example: /var/lib/mail/helpdesk/",
max_length=200,
null=True,
verbose_name="E-Mail Local Directory",
),
),
migrations.AddField(
model_name='queue',
name='logging_dir',
field=models.CharField(blank=True, help_text='If logging is enabled, what directory should we use to store log files for this queue? The standard logging mechanims are used if no directory is set', max_length=200, null=True, verbose_name='Logging Directory'),
model_name="queue",
name="logging_dir",
field=models.CharField(
blank=True,
help_text="If logging is enabled, what directory should we use to store log files for this queue? The standard logging mechanims are used if no directory is set",
max_length=200,
null=True,
verbose_name="Logging Directory",
),
),
migrations.AddField(
model_name='queue',
name='logging_type',
field=models.CharField(blank=True, choices=[('none', 'None'), ('debug', 'Debug'), ('info', 'Information'), ('warn', 'Warning'), ('error', 'Error'), ('crit', 'Critical')], help_text='Set the default logging level. All messages at that level or above will be logged to the directory set below. If no level is set, logging will be disabled.', max_length=5, null=True, verbose_name='Logging Type'),
model_name="queue",
name="logging_type",
field=models.CharField(
blank=True,
choices=[
("none", "None"),
("debug", "Debug"),
("info", "Information"),
("warn", "Warning"),
("error", "Error"),
("crit", "Critical"),
],
help_text="Set the default logging level. All messages at that level or above will be logged to the directory set below. If no level is set, logging will be disabled.",
max_length=5,
null=True,
verbose_name="Logging Type",
),
),
migrations.AlterField(
model_name='queue',
name='email_box_type',
field=models.CharField(blank=True, choices=[('pop3', 'POP 3'), ('imap', 'IMAP'), ('local', 'Local Directory')], help_text='E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.', max_length=5, null=True, verbose_name='E-Mail Box Type'),
model_name="queue",
name="email_box_type",
field=models.CharField(
blank=True,
choices=[
("pop3", "POP 3"),
("imap", "IMAP"),
("local", "Local Directory"),
],
help_text="E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.",
max_length=5,
null=True,
verbose_name="E-Mail Box Type",
),
),
]

View File

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

View File

@ -4,15 +4,21 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0014_usersettings_related_name'),
("helpdesk", "0014_usersettings_related_name"),
]
operations = [
migrations.AlterField(
model_name='queue',
name='permission_name',
field=models.CharField(blank=True, editable=False, help_text='Name used in the django.contrib.auth permission system', max_length=72, null=True, verbose_name='Django auth permission name'),
model_name="queue",
name="permission_name",
field=models.CharField(
blank=True,
editable=False,
help_text="Name used in the django.contrib.auth permission system",
max_length=72,
null=True,
verbose_name="Django auth permission name",
),
),
]

View File

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

View File

@ -6,15 +6,21 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0016_alter_model_options'),
("helpdesk", "0016_alter_model_options"),
]
operations = [
migrations.AlterField(
model_name='queue',
name='default_owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_owner', to=settings.AUTH_USER_MODEL, verbose_name='Default owner'),
model_name="queue",
name="default_owner",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="default_owner",
to=settings.AUTH_USER_MODEL,
verbose_name="Default owner",
),
),
]

View File

@ -4,55 +4,99 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0017_default_owner_on_delete_null'),
("helpdesk", "0017_default_owner_on_delete_null"),
]
operations = [
migrations.AlterField(
model_name='followup',
name='public',
field=models.BooleanField(blank=True, default=False, help_text='Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.', verbose_name='Public'),
model_name="followup",
name="public",
field=models.BooleanField(
blank=True,
default=False,
help_text="Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.",
verbose_name="Public",
),
),
migrations.AlterField(
model_name='ignoreemail',
name='keep_in_mailbox',
field=models.BooleanField(blank=True, default=False, help_text='Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.', verbose_name='Save Emails in Mailbox?'),
model_name="ignoreemail",
name="keep_in_mailbox",
field=models.BooleanField(
blank=True,
default=False,
help_text="Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.",
verbose_name="Save Emails in Mailbox?",
),
),
migrations.AlterField(
model_name='queue',
name='allow_email_submission',
field=models.BooleanField(blank=True, default=False, help_text='Do you want to poll the e-mail box below for new tickets?', verbose_name='Allow E-Mail Submission?'),
model_name="queue",
name="allow_email_submission",
field=models.BooleanField(
blank=True,
default=False,
help_text="Do you want to poll the e-mail box below for new tickets?",
verbose_name="Allow E-Mail Submission?",
),
),
migrations.AlterField(
model_name='queue',
name='allow_public_submission',
field=models.BooleanField(blank=True, default=False, help_text='Should this queue be listed on the public submission form?', verbose_name='Allow Public Submission?'),
model_name="queue",
name="allow_public_submission",
field=models.BooleanField(
blank=True,
default=False,
help_text="Should this queue be listed on the public submission form?",
verbose_name="Allow Public Submission?",
),
),
migrations.AlterField(
model_name='queue',
name='email_box_ssl',
field=models.BooleanField(blank=True, default=False, help_text='Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.', verbose_name='Use SSL for E-Mail?'),
model_name="queue",
name="email_box_ssl",
field=models.BooleanField(
blank=True,
default=False,
help_text="Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.",
verbose_name="Use SSL for E-Mail?",
),
),
migrations.AlterField(
model_name='savedsearch',
name='shared',
field=models.BooleanField(blank=True, default=False, help_text='Should other users see this query?', verbose_name='Shared With Other Users?'),
model_name="savedsearch",
name="shared",
field=models.BooleanField(
blank=True,
default=False,
help_text="Should other users see this query?",
verbose_name="Shared With Other Users?",
),
),
migrations.AlterField(
model_name='ticket',
name='on_hold',
field=models.BooleanField(blank=True, default=False, help_text='If a ticket is on hold, it will not automatically be escalated.', verbose_name='On Hold'),
model_name="ticket",
name="on_hold",
field=models.BooleanField(
blank=True,
default=False,
help_text="If a ticket is on hold, it will not automatically be escalated.",
verbose_name="On Hold",
),
),
migrations.AlterField(
model_name='ticketcc',
name='can_update',
field=models.BooleanField(blank=True, default=False, help_text='Can this CC login and update the ticket?', verbose_name='Can Update Ticket?'),
model_name="ticketcc",
name="can_update",
field=models.BooleanField(
blank=True,
default=False,
help_text="Can this CC login and update the ticket?",
verbose_name="Can Update Ticket?",
),
),
migrations.AlterField(
model_name='ticketcc',
name='can_view',
field=models.BooleanField(blank=True, default=False, help_text='Can this CC login to view the ticket details?', verbose_name='Can View Ticket?'),
model_name="ticketcc",
name="can_view",
field=models.BooleanField(
blank=True,
default=False,
help_text="Can this CC login to view the ticket details?",
verbose_name="Can View Ticket?",
),
),
]

View File

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

View File

@ -16,7 +16,7 @@ def unpickle_settings(settings_pickled):
# Python 3 support
from base64 import decodebytes as b64decode
try:
return pickle.loads(b64decode(settings_pickled.encode('utf-8')))
return pickle.loads(b64decode(settings_pickled.encode("utf-8")))
except Exception:
return {}
@ -33,41 +33,66 @@ def move_old_values(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0019_ticket_secret_key'),
("helpdesk", "0019_ticket_secret_key"),
]
operations = [
migrations.AddField(
model_name='usersettings',
name='email_on_ticket_assign',
field=models.BooleanField(default=helpdesk.models.email_on_ticket_assign_default, help_text='If you are assigned a ticket via the web, do you want to receive an e-mail?', verbose_name='E-mail me when assigned a ticket?'),
model_name="usersettings",
name="email_on_ticket_assign",
field=models.BooleanField(
default=helpdesk.models.email_on_ticket_assign_default,
help_text="If you are assigned a ticket via the web, do you want to receive an e-mail?",
verbose_name="E-mail me when assigned a ticket?",
),
),
migrations.AddField(
model_name='usersettings',
name='email_on_ticket_change',
field=models.BooleanField(default=helpdesk.models.email_on_ticket_change_default, help_text="If you're the ticket owner and the ticket is changed via the web by somebody else, do you want to receive an e-mail?", verbose_name='E-mail me on ticket change?'),
model_name="usersettings",
name="email_on_ticket_change",
field=models.BooleanField(
default=helpdesk.models.email_on_ticket_change_default,
help_text="If you're the ticket owner and the ticket is changed via the web by somebody else, do you want to receive an e-mail?",
verbose_name="E-mail me on ticket change?",
),
),
migrations.AddField(
model_name='usersettings',
name='login_view_ticketlist',
field=models.BooleanField(default=helpdesk.models.login_view_ticketlist_default, help_text='Display the ticket list upon login? Otherwise, the dashboard is shown.', verbose_name='Show Ticket List on Login?'),
model_name="usersettings",
name="login_view_ticketlist",
field=models.BooleanField(
default=helpdesk.models.login_view_ticketlist_default,
help_text="Display the ticket list upon login? Otherwise, the dashboard is shown.",
verbose_name="Show Ticket List on Login?",
),
),
migrations.AddField(
model_name='usersettings',
name='tickets_per_page',
field=models.IntegerField(choices=[(10, '10'), (25, '25'), (50, '50'), (100, '100')], default=helpdesk.models.tickets_per_page_default, help_text='How many tickets do you want to see on the Ticket List page?', verbose_name='Number of tickets to show per page'),
model_name="usersettings",
name="tickets_per_page",
field=models.IntegerField(
choices=[(10, "10"), (25, "25"), (50, "50"), (100, "100")],
default=helpdesk.models.tickets_per_page_default,
help_text="How many tickets do you want to see on the Ticket List page?",
verbose_name="Number of tickets to show per page",
),
),
migrations.AddField(
model_name='usersettings',
name='use_email_as_submitter',
field=models.BooleanField(default=helpdesk.models.use_email_as_submitter_default, help_text='When you submit a ticket, do you want to automatically use your e-mail address as the submitter address? You can type a different e-mail address when entering the ticket if needed, this option only changes the default.', verbose_name='Use my e-mail address when submitting tickets?'),
model_name="usersettings",
name="use_email_as_submitter",
field=models.BooleanField(
default=helpdesk.models.use_email_as_submitter_default,
help_text="When you submit a ticket, do you want to automatically use your e-mail address as the submitter address? You can type a different e-mail address when entering the ticket if needed, this option only changes the default.",
verbose_name="Use my e-mail address when submitting tickets?",
),
),
migrations.AlterField(
model_name='usersettings',
name='settings_pickled',
field=models.TextField(blank=True, help_text='DEPRECATED! This is a base64-encoded representation of a pickled Python dictionary. Do not change this field via the admin.', null=True, verbose_name='DEPRECATED! Settings Dictionary DEPRECATED!'),
model_name="usersettings",
name="settings_pickled",
field=models.TextField(
blank=True,
help_text="DEPRECATED! This is a base64-encoded representation of a pickled Python dictionary. Do not change this field via the admin.",
null=True,
verbose_name="DEPRECATED! Settings Dictionary DEPRECATED!",
),
),
migrations.RunPython(move_old_values),
]

View File

@ -4,61 +4,105 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('helpdesk', '0020_depickle_user_settings'),
("helpdesk", "0020_depickle_user_settings"),
]
operations = [
migrations.AddField(
model_name='kbitem',
name='voted_by',
model_name="kbitem",
name="voted_by",
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='followup',
name='public',
field=models.BooleanField(blank=True, default=False, help_text='Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.', verbose_name='Public'),
model_name="followup",
name="public",
field=models.BooleanField(
blank=True,
default=False,
help_text="Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.",
verbose_name="Public",
),
),
migrations.AlterField(
model_name='ignoreemail',
name='keep_in_mailbox',
field=models.BooleanField(blank=True, default=False, help_text='Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.', verbose_name='Save Emails in Mailbox?'),
model_name="ignoreemail",
name="keep_in_mailbox",
field=models.BooleanField(
blank=True,
default=False,
help_text="Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.",
verbose_name="Save Emails in Mailbox?",
),
),
migrations.AlterField(
model_name='queue',
name='allow_email_submission',
field=models.BooleanField(blank=True, default=False, help_text='Do you want to poll the e-mail box below for new tickets?', verbose_name='Allow E-Mail Submission?'),
model_name="queue",
name="allow_email_submission",
field=models.BooleanField(
blank=True,
default=False,
help_text="Do you want to poll the e-mail box below for new tickets?",
verbose_name="Allow E-Mail Submission?",
),
),
migrations.AlterField(
model_name='queue',
name='allow_public_submission',
field=models.BooleanField(blank=True, default=False, help_text='Should this queue be listed on the public submission form?', verbose_name='Allow Public Submission?'),
model_name="queue",
name="allow_public_submission",
field=models.BooleanField(
blank=True,
default=False,
help_text="Should this queue be listed on the public submission form?",
verbose_name="Allow Public Submission?",
),
),
migrations.AlterField(
model_name='queue',
name='email_box_ssl',
field=models.BooleanField(blank=True, default=False, help_text='Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.', verbose_name='Use SSL for E-Mail?'),
model_name="queue",
name="email_box_ssl",
field=models.BooleanField(
blank=True,
default=False,
help_text="Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.",
verbose_name="Use SSL for E-Mail?",
),
),
migrations.AlterField(
model_name='savedsearch',
name='shared',
field=models.BooleanField(blank=True, default=False, help_text='Should other users see this query?', verbose_name='Shared With Other Users?'),
model_name="savedsearch",
name="shared",
field=models.BooleanField(
blank=True,
default=False,
help_text="Should other users see this query?",
verbose_name="Shared With Other Users?",
),
),
migrations.AlterField(
model_name='ticket',
name='on_hold',
field=models.BooleanField(blank=True, default=False, help_text='If a ticket is on hold, it will not automatically be escalated.', verbose_name='On Hold'),
model_name="ticket",
name="on_hold",
field=models.BooleanField(
blank=True,
default=False,
help_text="If a ticket is on hold, it will not automatically be escalated.",
verbose_name="On Hold",
),
),
migrations.AlterField(
model_name='ticketcc',
name='can_update',
field=models.BooleanField(blank=True, default=False, help_text='Can this CC login and update the ticket?', verbose_name='Can Update Ticket?'),
model_name="ticketcc",
name="can_update",
field=models.BooleanField(
blank=True,
default=False,
help_text="Can this CC login and update the ticket?",
verbose_name="Can Update Ticket?",
),
),
migrations.AlterField(
model_name='ticketcc',
name='can_view',
field=models.BooleanField(blank=True, default=False, help_text='Can this CC login to view the ticket details?', verbose_name='Can View Ticket?'),
model_name="ticketcc",
name="can_view",
field=models.BooleanField(
blank=True,
default=False,
help_text="Can this CC login to view the ticket details?",
verbose_name="Can View Ticket?",
),
),
]

View File

@ -4,15 +4,21 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0021_voting_tracker'),
("helpdesk", "0021_voting_tracker"),
]
operations = [
migrations.AddField(
model_name='followup',
name='message_id',
field=models.CharField(blank=True, editable=False, help_text="The Message ID of the submitter's email.", max_length=256, null=True, verbose_name='E-Mail ID'),
model_name="followup",
name="message_id",
field=models.CharField(
blank=True,
editable=False,
help_text="The Message ID of the submitter's email.",
max_length=256,
null=True,
verbose_name="E-Mail ID",
),
),
]

View File

@ -4,15 +4,18 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0022_add_submitter_email_id_field_to_ticket'),
("helpdesk", "0022_add_submitter_email_id_field_to_ticket"),
]
operations = [
migrations.AddField(
model_name='queue',
name='enable_notifications_on_email_events',
field=models.BooleanField(default=False, help_text='When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature', verbose_name='Notify contacts when email updates arrive'),
model_name="queue",
name="enable_notifications_on_email_events",
field=models.BooleanField(
default=False,
help_text="When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature",
verbose_name="Notify contacts when email updates arrive",
),
),
]

View File

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

View File

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

View File

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

View File

@ -6,66 +6,98 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('helpdesk', '0026_kbitem_attachments'),
("helpdesk", "0026_kbitem_attachments"),
]
operations = [
migrations.AddField(
model_name='kbcategory',
name='queue',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='helpdesk.Queue', verbose_name='Default queue when creating a ticket after viewing this category.'),
model_name="kbcategory",
name="queue",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="helpdesk.Queue",
verbose_name="Default queue when creating a ticket after viewing this category.",
),
),
migrations.AddField(
model_name='kbitem',
name='downvoted_by',
field=models.ManyToManyField(related_name='downvotes', to=settings.AUTH_USER_MODEL),
model_name="kbitem",
name="downvoted_by",
field=models.ManyToManyField(
related_name="downvotes", to=settings.AUTH_USER_MODEL
),
),
migrations.AddField(
model_name='ticket',
name='kbitem',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='helpdesk.KBItem', verbose_name='Knowledge base item the user was viewing when they created this ticket.'),
model_name="ticket",
name="kbitem",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="helpdesk.KBItem",
verbose_name="Knowledge base item the user was viewing when they created this ticket.",
),
),
migrations.AlterField(
model_name='followupattachment',
name='filename',
field=models.CharField(blank=True, max_length=1000, verbose_name='Filename'),
model_name="followupattachment",
name="filename",
field=models.CharField(
blank=True, max_length=1000, verbose_name="Filename"
),
),
migrations.AlterField(
model_name='followupattachment',
name='mime_type',
field=models.CharField(blank=True, max_length=255, verbose_name='MIME Type'),
model_name="followupattachment",
name="mime_type",
field=models.CharField(
blank=True, max_length=255, verbose_name="MIME Type"
),
),
migrations.AlterField(
model_name='followupattachment',
name='size',
field=models.IntegerField(blank=True, help_text='Size of this file in bytes', verbose_name='Size'),
model_name="followupattachment",
name="size",
field=models.IntegerField(
blank=True, help_text="Size of this file in bytes", verbose_name="Size"
),
),
migrations.AlterField(
model_name='kbiattachment',
name='filename',
field=models.CharField(blank=True, max_length=1000, verbose_name='Filename'),
model_name="kbiattachment",
name="filename",
field=models.CharField(
blank=True, max_length=1000, verbose_name="Filename"
),
),
migrations.AlterField(
model_name='kbiattachment',
name='mime_type',
field=models.CharField(blank=True, max_length=255, verbose_name='MIME Type'),
model_name="kbiattachment",
name="mime_type",
field=models.CharField(
blank=True, max_length=255, verbose_name="MIME Type"
),
),
migrations.AlterField(
model_name='kbiattachment',
name='size',
field=models.IntegerField(blank=True, help_text='Size of this file in bytes', verbose_name='Size'),
model_name="kbiattachment",
name="size",
field=models.IntegerField(
blank=True, help_text="Size of this file in bytes", verbose_name="Size"
),
),
migrations.AlterField(
model_name='kbitem',
name='voted_by',
field=models.ManyToManyField(related_name='votes', to=settings.AUTH_USER_MODEL),
model_name="kbitem",
name="voted_by",
field=models.ManyToManyField(
related_name="votes", to=settings.AUTH_USER_MODEL
),
),
migrations.AlterField(
model_name='queue',
name='enable_notifications_on_email_events',
field=models.BooleanField(blank=True, default=False, help_text='When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature', verbose_name='Notify contacts when email updates arrive'),
model_name="queue",
name="enable_notifications_on_email_events",
field=models.BooleanField(
blank=True,
default=False,
help_text="When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature",
verbose_name="Notify contacts when email updates arrive",
),
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,15 +5,21 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0032_kbitem_enabled'),
("helpdesk", "0032_kbitem_enabled"),
]
operations = [
migrations.AddField(
model_name='ticket',
name='merged_to',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='merged_tickets', to='helpdesk.Ticket', verbose_name='merged to'),
model_name="ticket",
name="merged_to",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="merged_tickets",
to="helpdesk.Ticket",
verbose_name="merged to",
),
),
]

View File

@ -7,10 +7,12 @@ def forwards_func(apps, schema_editor):
EmailTemplate = apps.get_model("helpdesk", "EmailTemplate")
db_alias = schema_editor.connection.alias
EmailTemplate.objects.using(db_alias).create(
id=EmailTemplate.objects.using(db_alias).order_by('-id').first().id + 1 if EmailTemplate.objects.using(db_alias).first() else 1, # because PG sequences are not reset
template_name='merged',
subject='(Merged)',
heading='Ticket merged',
id=EmailTemplate.objects.using(db_alias).order_by("-id").first().id + 1
if EmailTemplate.objects.using(db_alias).first()
else 1, # because PG sequences are not reset
template_name="merged",
subject="(Merged)",
heading="Ticket merged",
plain_text="""Hello,
This is a courtesy e-mail to let you know that ticket {{ ticket.ticket }} ("{{ ticket.title }}") by {{ ticket.submitter_email }} has been merged to ticket {{ ticket.merged_to.ticket }}.
@ -21,13 +23,14 @@ From now on, please answer on this ticket, or you can include the tag {{ ticket.
<p style="font-family: sans-serif; font-size: 1em;">This is a courtesy e-mail to let you know that ticket <b>{{ ticket.ticket }}</b> (<em>{{ ticket.title }}</em>) by {{ ticket.submitter_email }} has been merged to ticket <a href="{{ ticket.merged_to.staff_url }}">{{ ticket.merged_to.ticket }}</a>.</p>
<p style="font-family: sans-serif; font-size: 1em;">From now on, please answer on this ticket, or you can include the tag <b>{{ ticket.merged_to.ticket }}</b> in your e-mail subject.</p>""",
locale='en'
locale="en",
)
EmailTemplate.objects.using(db_alias).create(
id=EmailTemplate.objects.using(db_alias).order_by('-id').first().id + 1, # because PG sequences are not reset
template_name='merged',
subject='(Fusionné)',
heading='Ticket Fusionné',
id=EmailTemplate.objects.using(db_alias).order_by("-id").first().id
+ 1, # because PG sequences are not reset
template_name="merged",
subject="(Fusionné)",
heading="Ticket Fusionné",
plain_text="""Bonjour,
Ce courriel indicatif permet de vous prévenir que le ticket {{ ticket.ticket }} ("{{ ticket.title }}") par {{ ticket.submitter_email }} a été fusionné au ticket {{ ticket.merged_to.ticket }}.
@ -38,20 +41,19 @@ Veillez à répondre sur ce ticket dorénavant, ou bien inclure la balise {{ tic
<p style="font-family: sans-serif; font-size: 1em;">Ce courriel indicatif permet de vous prévenir que le ticket <b>{{ ticket.ticket }}</b> (<em>{{ ticket.title }}</em>) par {{ ticket.submitter_email }} a été fusionné au ticket <a href="{{ ticket.merged_to.staff_url }}">{{ ticket.merged_to.ticket }}</a>.</p>
<p style="font-family: sans-serif; font-size: 1em;">Veillez à répondre sur ce ticket dorénavant, ou bien inclure la balise <b>{{ ticket.merged_to.ticket }}</b> dans le sujet de votre réponse par mail.</p>""",
locale='fr'
locale="fr",
)
def reverse_func(apps, schema_editor):
EmailTemplate = apps.get_model("helpdesk", "EmailTemplate")
db_alias = schema_editor.connection.alias
EmailTemplate.objects.using(db_alias).filter(template_name='merged').delete()
EmailTemplate.objects.using(db_alias).filter(template_name="merged").delete()
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0033_ticket_merged_to'),
("helpdesk", "0033_ticket_merged_to"),
]
operations = [

View File

@ -5,15 +5,18 @@ import helpdesk.models
class Migration(migrations.Migration):
dependencies = [
("helpdesk", "0034_create_email_template_for_merged"),
]
dependencies = [
('helpdesk', '0034_create_email_template_for_merged'),
]
operations = [
operations = [
migrations.AlterField(
model_name='usersettings',
name='email_on_ticket_change',
field=models.BooleanField(default=helpdesk.models.email_on_ticket_change_default, help_text="If you're the ticket owner and the ticket is changed via the web by somebody else,do you want to receive an e-mail?", verbose_name='E-mail me on ticket change?'),
model_name="usersettings",
name="email_on_ticket_change",
field=models.BooleanField(
default=helpdesk.models.email_on_ticket_change_default,
help_text="If you're the ticket owner and the ticket is changed via the web by somebody else,do you want to receive an e-mail?",
verbose_name="E-mail me on ticket change?",
),
),
]
]

View File

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

View File

@ -4,15 +4,26 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0036_add_attachment_validator'),
("helpdesk", "0036_add_attachment_validator"),
]
operations = [
migrations.AlterField(
model_name='queue',
name='email_box_type',
field=models.CharField(blank=True, choices=[('pop3', 'POP 3'), ('imap', 'IMAP'), ('oauth', 'IMAP OAUTH'), ('local', 'Local Directory')], help_text='E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.', max_length=5, null=True, verbose_name='E-Mail Box Type'),
model_name="queue",
name="email_box_type",
field=models.CharField(
blank=True,
choices=[
("pop3", "POP 3"),
("imap", "IMAP"),
("oauth", "IMAP OAUTH"),
("local", "Local Directory"),
],
help_text="E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.",
max_length=5,
null=True,
verbose_name="E-Mail Box Type",
),
),
]

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -37,52 +37,52 @@ class KBTests(TestCase):
self.user = get_staff_user()
def test_kb_index(self):
response = self.client.get(reverse('helpdesk:kb_index'))
self.assertContains(response, 'This is a test category')
response = self.client.get(reverse("helpdesk:kb_index"))
self.assertContains(response, "This is a test category")
def test_kb_category(self):
response = self.client.get(
reverse('helpdesk:kb_category', args=("test_cat", )))
self.assertContains(response, 'This is a test category')
self.assertContains(response, 'KBItem 1')
self.assertContains(response, 'KBItem 2')
self.assertContains(response, 'Create New Ticket Queue:')
self.client.login(username=self.user.get_username(),
password='password')
response = self.client.get(
reverse('helpdesk:kb_category', args=("test_cat", )))
response = self.client.get(reverse("helpdesk:kb_category", args=("test_cat",)))
self.assertContains(response, "This is a test category")
self.assertContains(response, "KBItem 1")
self.assertContains(response, "KBItem 2")
self.assertContains(response, "Create New Ticket Queue:")
self.client.login(username=self.user.get_username(), password="password")
response = self.client.get(reverse("helpdesk:kb_category", args=("test_cat",)))
self.assertContains(response, '<i class="fa fa-thumbs-up fa-lg"></i>')
self.assertContains(response, '0 open tickets')
self.assertContains(response, "0 open tickets")
ticket = Ticket.objects.create(
title="Test ticket",
queue=self.queue,
kbitem=self.kbitem1,
)
ticket.save()
response = self.client.get(
reverse('helpdesk:kb_category', args=("test_cat",)))
self.assertContains(response, '1 open tickets')
response = self.client.get(reverse("helpdesk:kb_category", args=("test_cat",)))
self.assertContains(response, "1 open tickets")
def test_kb_vote(self):
self.client.login(username=self.user.get_username(),
password='password')
self.client.login(username=self.user.get_username(), password="password")
response = self.client.post(
reverse('helpdesk:kb_vote', args=(self.kbitem1.pk, "up")), params={})
cat_url = reverse('helpdesk:kb_category',
args=("test_cat",)) + "?kbitem=1"
reverse("helpdesk:kb_vote", args=(self.kbitem1.pk, "up")), params={}
)
cat_url = reverse("helpdesk:kb_category", args=("test_cat",)) + "?kbitem=1"
self.assertRedirects(response, cat_url)
response = self.client.get(cat_url)
self.assertContains(response, '1 people found this answer useful of 1')
self.assertContains(response, "1 people found this answer useful of 1")
response = self.client.post(
reverse('helpdesk:kb_vote', args=(self.kbitem1.pk, "down")), params={})
reverse("helpdesk:kb_vote", args=(self.kbitem1.pk, "down")), params={}
)
self.assertRedirects(response, cat_url)
response = self.client.get(cat_url)
self.assertContains(response, '0 people found this answer useful of 1')
self.assertContains(response, "0 people found this answer useful of 1")
def test_kb_category_iframe(self):
cat_url = reverse('helpdesk:kb_category', args=(
"test_cat",)) + "?kbitem=1&submitter_email=foo@bar.cz&title=lol&"
cat_url = (
reverse("helpdesk:kb_category", args=("test_cat",))
+ "?kbitem=1&submitter_email=foo@bar.cz&title=lol&"
)
response = self.client.get(cat_url)
# Assert that query params are passed on to ticket submit form
self.assertContains(
response, "'/tickets/submit/?queue=1&_readonly_fields_=queue&kbitem=1&submitter_email=foo%40bar.cz&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):
@override_settings(LOGIN_URL='/custom/login/')
@override_settings(LOGIN_URL="/custom/login/")
def test_custom_login_view_with_url(self):
"""Test login redirect when LOGIN_URL is set to custom url"""
response = self.client.get(reverse('helpdesk:login'))
response = self.client.get(reverse("helpdesk:login"))
# We expect that that helpdesk:home url is passed as next parameter in
# the redirect url, so that the custom login can redirect the browser
# back to helpdesk after the login.
home_url = reverse('helpdesk:home')
expected = '/custom/login/?next={}'.format(home_url)
home_url = reverse("helpdesk:home")
expected = "/custom/login/?next={}".format(home_url)
self.assertRedirects(response, expected, fetch_redirect_response=False)
@override_settings(LOGIN_URL='/custom/login/')
@override_settings(LOGIN_URL="/custom/login/")
def test_custom_login_next_param(self):
"""Test that the next url parameter is correctly relayed to custom login"""
next_param = "/redirect/back"
url = reverse('helpdesk:login') + "?next=" + next_param
url = reverse("helpdesk:login") + "?next=" + next_param
response = self.client.get(url)
expected = '/custom/login/?next={}'.format(next_param)
expected = "/custom/login/?next={}".format(next_param)
self.assertRedirects(response, expected, fetch_redirect_response=False)
@override_settings(LOGIN_URL='helpdesk:login', SITE_ID=1)
@override_settings(LOGIN_URL="helpdesk:login", SITE_ID=1)
def test_default_login_view(self):
"""Test that default login is used when LOGIN_URL is helpdesk:login"""
response = self.client.get(reverse('helpdesk:login'))
self.assertTemplateUsed(response, 'helpdesk/registration/login.html')
response = self.client.get(reverse("helpdesk:login"))
self.assertTemplateUsed(response, "helpdesk/registration/login.html")
@override_settings(LOGIN_URL=None, SITE_ID=1)
def test_login_url_none(self):
"""Test that default login is used when LOGIN_URL is None"""
response = self.client.get(reverse('helpdesk:login'))
self.assertTemplateUsed(response, 'helpdesk/registration/login.html')
response = self.client.get(reverse("helpdesk:login"))
self.assertTemplateUsed(response, "helpdesk/registration/login.html")
@override_settings(LOGIN_URL='admin:login', SITE_ID=1)
@override_settings(LOGIN_URL="admin:login", SITE_ID=1)
def test_custom_login_view_with_name(self):
"""Test that LOGIN_URL can be a view name"""
response = self.client.get(reverse('helpdesk:login'))
home_url = reverse('helpdesk:home')
expected = reverse('admin:login') + "?next=" + home_url
response = self.client.get(reverse("helpdesk:login"))
home_url = reverse("helpdesk:home")
expected = reverse("admin:login") + "?next=" + home_url
self.assertRedirects(response, expected)

View File

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

View File

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

View File

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

View File

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

View File

@ -47,25 +47,57 @@ class QueryTests(TestCase):
"""Create a staff user and login"""
User = get_user_model()
self.user = User.objects.create(
username='User_1',
username="User_1",
is_staff=is_staff,
)
self.user.set_password('pass')
self.user.set_password("pass")
self.user.save()
self.client.login(username='User_1', password='pass')
self.client.login(username="User_1", password="pass")
def test_query_basic(self):
self.loginUser()
query = query_to_base64({})
response = self.client.get(
reverse('helpdesk:datatables_ticket_list', args=[query]))
reverse("helpdesk:datatables_ticket_list", args=[query])
)
resp_json = response.json()
self.assertEqual(
resp_json,
{
"data":
[{"ticket": "1 [test_queue-1]", "id": 1, "priority": 3, "title": "unassigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": ""},
{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": resp_json["data"][1]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
"data": [
{
"ticket": "1 [test_queue-1]",
"id": 1,
"priority": 3,
"title": "unassigned to kbitem",
"queue": {"title": "Test queue", "id": 1},
"status": "Open",
"created": resp_json["data"][0]["created"],
"due_date": None,
"assigned_to": "None",
"submitter": None,
"last_followup": None,
"row_class": "",
"time_spent": "",
"kbitem": "",
},
{
"ticket": "2 [test_queue-2]",
"id": 2,
"priority": 3,
"title": "assigned to kbitem",
"queue": {"title": "Test queue", "id": 1},
"status": "Open",
"created": resp_json["data"][1]["created"],
"due_date": None,
"assigned_to": "None",
"submitter": None,
"last_followup": None,
"row_class": "",
"time_spent": "",
"kbitem": "KBItem 1",
},
],
"recordsFiltered": 2,
"recordsTotal": 2,
"draw": 0,
@ -74,18 +106,32 @@ class QueryTests(TestCase):
def test_query_by_kbitem(self):
self.loginUser()
query = query_to_base64(
{'filtering': {'kbitem__in': [self.kbitem1.pk]}}
)
query = query_to_base64({"filtering": {"kbitem__in": [self.kbitem1.pk]}})
response = self.client.get(
reverse('helpdesk:datatables_ticket_list', args=[query]))
reverse("helpdesk:datatables_ticket_list", args=[query])
)
resp_json = response.json()
self.assertEqual(
resp_json,
{
"data":
[{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open",
"created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
"data": [
{
"ticket": "2 [test_queue-2]",
"id": 2,
"priority": 3,
"title": "assigned to kbitem",
"queue": {"title": "Test queue", "id": 1},
"status": "Open",
"created": resp_json["data"][0]["created"],
"due_date": None,
"assigned_to": "None",
"submitter": None,
"last_followup": None,
"row_class": "",
"time_spent": "",
"kbitem": "KBItem 1",
}
],
"recordsFiltered": 1,
"recordsTotal": 1,
"draw": 0,
@ -94,18 +140,32 @@ class QueryTests(TestCase):
def test_query_by_no_kbitem(self):
self.loginUser()
query = query_to_base64(
{'filtering_null': {'kbitem__isnull': True}}
)
query = query_to_base64({"filtering_null": {"kbitem__isnull": True}})
response = self.client.get(
reverse('helpdesk:datatables_ticket_list', args=[query]))
reverse("helpdesk:datatables_ticket_list", args=[query])
)
resp_json = response.json()
self.assertEqual(
resp_json,
{
"data":
[{"ticket": "1 [test_queue-1]", "id": 1, "priority": 3, "title": "unassigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open",
"created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": ""}],
"data": [
{
"ticket": "1 [test_queue-1]",
"id": 1,
"priority": 3,
"title": "unassigned to kbitem",
"queue": {"title": "Test queue", "id": 1},
"status": "Open",
"created": resp_json["data"][0]["created"],
"due_date": None,
"assigned_to": "None",
"submitter": None,
"last_followup": None,
"row_class": "",
"time_spent": "",
"kbitem": "",
}
],
"recordsFiltered": 1,
"recordsTotal": 1,
"draw": 0,

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -4,20 +4,18 @@ from django.urls import reverse
class TicketActionsTestCase(TestCase):
fixtures = ['emailtemplate.json']
fixtures = ["emailtemplate.json"]
def setUp(self):
User = get_user_model()
self.user = User.objects.create(
username='User_1',
username="User_1",
is_staff=True,
)
self.user.set_password('pass')
self.user.set_password("pass")
self.user.save()
self.client.login(username='User_1', password='pass')
self.client.login(username="User_1", password="pass")
def test_get_user_settings(self):
response = self.client.get(
reverse('helpdesk:user_settings'), follow=True)
response = self.client.get(reverse("helpdesk:user_settings"), follow=True)
self.assertContains(response, "Use the following options")

View File

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

View File

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

View File

@ -1,6 +1,5 @@
"""UItility functions facilitate making unit testing easier and less brittle."""
from PIL import Image
import email
from email import encoders
@ -31,8 +30,8 @@ def strip_accents(text):
:returns: The processed String.
:rtype: String.
"""
text = unicodedata.normalize('NFD', text)
text = text.encode('ascii', 'ignore')
text = unicodedata.normalize("NFD", text)
text = text.encode("ascii", "ignore")
text = text.decode("utf-8")
return str(text)
@ -48,12 +47,12 @@ def text_to_id(text):
:rtype: String.
"""
text = strip_accents(text.lower())
text = re.sub('[ ]+', '_', text)
text = re.sub('[^0-9a-zA-Z_-]', '', text)
text = re.sub("[ ]+", "_", text)
text = re.sub("[^0-9a-zA-Z_-]", "", text)
return text
def get_random_string(length: int=16) -> str:
def get_random_string(length: int = 16) -> str:
return "".join(
[random.choice(string.ascii_letters + string.digits) for _ in range(length)]
)
@ -62,27 +61,27 @@ def get_random_string(length: int=16) -> str:
def generate_random_image(image_format, array_dims):
"""
Creates an image from a random array.
:param image_format: An image format (PNG or JPEG).
:param array_dims: A tuple with array dimensions.
:returns: A byte string with encoded image
:rtype: bytes
"""
image_bytes = randint(low=0, high=255, size=array_dims, dtype='uint8')
image_bytes = randint(low=0, high=255, size=array_dims, dtype="uint8")
io = BytesIO()
image_pil = Image.fromarray(image_bytes)
image_pil.save(io, image_format, subsampling=0, quality=100)
return io.getvalue()
def get_random_image(image_format: str="PNG", size: int=5):
def get_random_image(image_format: str = "PNG", size: int = 5):
"""
Returns a random image.
Args:
image_format: An image format (PNG or JPEG).
Returns:
A string with encoded image
"""
@ -92,120 +91,186 @@ def get_random_image(image_format: str="PNG", size: int=5):
def get_fake(provider: str, locale: str = "en_US", min_length: int = 5) -> Any:
"""
Generates a random string, float, integer etc based on provider
Provider can be "text', 'sentence', "word"
e.g. `get_fake('name')` ==> 'Buzz Aldrin'
Provider can be "text', 'sentence', "word"
e.g. `get_fake('name')` ==> 'Buzz Aldrin'
"""
string = factory.Faker(provider).evaluate({}, None, {'locale': locale,})
string = factory.Faker(provider).evaluate(
{},
None,
{
"locale": locale,
},
)
while len(string) < min_length:
string += factory.Faker(provider).evaluate({}, None, {'locale': locale,})
string += factory.Faker(provider).evaluate(
{},
None,
{
"locale": locale,
},
)
return string
def get_fake_html(locale: str = "en_US", wrap_in_body_tag=True) -> Any:
"""
Generates a random string, float, integer etc based on provider
Provider can be "text', 'sentence',
e.g. `get_fake('name')` ==> 'Buzz Aldrin'
Provider can be "text', 'sentence',
e.g. `get_fake('name')` ==> 'Buzz Aldrin'
"""
html = factory.Faker("sentence").evaluate({}, None, {'locale': locale,})
for _ in range(0,4):
html += "<li>" + factory.Faker("sentence").evaluate({}, None, {'locale': locale,}) + "</li>"
for _ in range(0,4):
html += "<p>" + factory.Faker("text").evaluate({}, None, {'locale': locale,})
html = factory.Faker("sentence").evaluate(
{},
None,
{
"locale": locale,
},
)
for _ in range(0, 4):
html += (
"<li>"
+ factory.Faker("sentence").evaluate(
{},
None,
{
"locale": locale,
},
)
+ "</li>"
)
for _ in range(0, 4):
html += "<p>" + factory.Faker("text").evaluate(
{},
None,
{
"locale": locale,
},
)
return f"<body>{html}</body>" if wrap_in_body_tag else html
def generate_email_address(
locale: str="en_US",
use_short_email: bool=False,
real_name_format: Optional[str]="{last_name}, {first_name}",
last_name_override: Optional[str]=None) -> Tuple[str, str, str, str]:
'''
locale: str = "en_US",
use_short_email: bool = False,
real_name_format: Optional[str] = "{last_name}, {first_name}",
last_name_override: Optional[str] = None,
) -> Tuple[str, str, str, str]:
"""
Generate an RFC 2822 email address
:param locale: change this to generate locale specific names
:param use_short_email: defaults to false. If true then does not include real name in email address
:param real_name_format: pass a different format if different than "{last_name}, {first_name}"
:param last_name_override: override the fake name if you want some special characters in the last name
:returns <RFC2822 formatted email for header>, <short email address>, <first name>, <last_name
'''
"""
fake = faker.Faker(locale=locale)
first_name = fake.first_name()
last_name = last_name_override or fake.last_name()
real_name = None if use_short_email else real_name_format.format(first_name=first_name, last_name=last_name)
real_name = (
None
if use_short_email
else real_name_format.format(first_name=first_name, last_name=last_name)
)
# Add a random string to ensure we do not generate a real domain name
email_address = "{}.{}@{}".format(
first_name.replace(' ', '').encode("ascii", "ignore").lower().decode(),
last_name.replace(' ', '').encode("ascii", "ignore").lower().decode(),
get_random_string(5) + fake.domain_name()
first_name.replace(" ", "").encode("ascii", "ignore").lower().decode(),
last_name.replace(" ", "").encode("ascii", "ignore").lower().decode(),
get_random_string(5) + fake.domain_name(),
)
# format email address for RFC 2822 and return
return email.utils.formataddr((real_name, email_address)), email_address, first_name, last_name
return (
email.utils.formataddr((real_name, email_address)),
email_address,
first_name,
last_name,
)
def generate_file_mime_part(locale: str="en_US",filename: str = None, content: str = None) -> Message:
def generate_file_mime_part(
locale: str = "en_US", filename: str = None, content: str = None
) -> Message:
"""
:param locale: change this to generate locale specific file name and attachment content
:param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated
:param content: pass a string value if you want have specific content otherwise a random string will be generated
"""
part = MIMEBase('application', 'octet-stream')
part.set_payload(get_fake("text", locale=locale, min_length=1024) if content is None else content)
part = MIMEBase("application", "octet-stream")
part.set_payload(
get_fake("text", locale=locale, min_length=1024) if content is None else content
)
encoders.encode_base64(part)
if not filename:
filename = get_fake("word", locale=locale, min_length=8) + ".txt"
part.add_header('Content-Disposition', "attachment; filename=%s" % filename)
part.add_header("Content-Disposition", "attachment; filename=%s" % filename)
return part
def generate_executable_mime_part(locale: str="en_US",filename: str = None, content: str = None) -> Message:
def generate_executable_mime_part(
locale: str = "en_US", filename: str = None, content: str = None
) -> Message:
"""
:param locale: change this to generate locale specific file name and attachment content
:param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated
:param content: pass a string value if you want have specific content otherwise a random string will be generated
"""
part = MIMEBase('application', 'vnd.microsoft.portable-executable')
part.set_payload(get_fake("text", locale=locale, min_length=1024) if content is None else content)
part = MIMEBase("application", "vnd.microsoft.portable-executable")
part.set_payload(
get_fake("text", locale=locale, min_length=1024) if content is None else content
)
encoders.encode_base64(part)
if not filename:
filename = get_fake("word", locale=locale, min_length=8) + ".exe"
part.add_header('Content-Disposition', "attachment; filename=%s" % filename)
part.add_header("Content-Disposition", "attachment; filename=%s" % filename)
return part
def generate_image_mime_part(locale: str="en_US",imagename: str = None, disposition_primary_type: str = "attachment") -> Message:
def generate_image_mime_part(
locale: str = "en_US",
imagename: str = None,
disposition_primary_type: str = "attachment",
) -> Message:
"""
:param locale: change this to generate locale specific file name and attachment content
:param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated
"""
part = MIMEImage(generate_random_image(image_format="JPEG", array_dims=(200, 200)))
#part.set_payload(get_fake("text", locale=locale, min_length=1024))
# part.set_payload(get_fake("text", locale=locale, min_length=1024))
encoders.encode_base64(part)
if not imagename:
imagename = get_fake("word", locale=locale, min_length=8) + ".jpg"
part.add_header('Content-Disposition', disposition_primary_type + "; filename= %s" % imagename)
part.add_header(
"Content-Disposition", disposition_primary_type + "; filename= %s" % imagename
)
return part
def generate_email_list(address_cnt: int = 3,
locale: str="en_US",
use_short_email: bool=False
) -> str:
def generate_email_list(
address_cnt: int = 3, locale: str = "en_US", use_short_email: bool = False
) -> str:
"""
Generates a list of email addresses formatted for email headers on a Mime part
:param address_cnt: the number of email addresses to string together
:param locale: change this to generate locale specific "real names" and subject
:param use_short_email: produces a email address without "real name" if True
"""
email_address_list = [generate_email_address(locale, use_short_email=use_short_email)[0] for _ in range(0, address_cnt)]
email_address_list = [
generate_email_address(locale, use_short_email=use_short_email)[0]
for _ in range(0, address_cnt)
]
return ",".join(email_address_list)
def add_simple_email_headers(message: Message, locale: str="en_US",
use_short_email: bool=False
) -> typing.Tuple[typing.Tuple[str, str], typing.Tuple[str, str]]:
def add_simple_email_headers(
message: Message, locale: str = "en_US", use_short_email: bool = False
) -> typing.Tuple[typing.Tuple[str, str], typing.Tuple[str, str]]:
"""
Adds the key email headers to a Mime part
:param message: the Mime part to add headers to
:param locale: change this to generate locale specific "real names" and subject
:param use_short_email: produces a "To" or "From" that is only the email address if True
@ -213,18 +278,20 @@ def add_simple_email_headers(message: Message, locale: str="en_US",
"""
to_meta = generate_email_address(locale, use_short_email=use_short_email)
from_meta = generate_email_address(locale, use_short_email=use_short_email)
message['Subject'] = get_fake("sentence", locale=locale)
message['From'] = from_meta[0]
message['To'] = to_meta[0]
message["Subject"] = get_fake("sentence", locale=locale)
message["From"] = from_meta[0]
message["To"] = to_meta[0]
return from_meta, to_meta
def generate_mime_part(locale: str="en_US",
part_type: str="plain",
) -> typing.Optional[Message]:
def generate_mime_part(
locale: str = "en_US",
part_type: str = "plain",
) -> typing.Optional[Message]:
"""
Generates amime part of the sepecified type
:param locale: change this to generate locale specific strings
:param text_type: options are plain, html, image (attachment), file (attachment)
"""
@ -244,43 +311,53 @@ def generate_mime_part(locale: str="en_US",
raise Exception("Mime part not implemented: " + part_type)
return msg
def generate_multipart_email(locale: str="en_US",
type_list: typing.List[str]=["plain", "html", "image"],
sub_type: str = None,
use_short_email: bool=False
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
def generate_multipart_email(
locale: str = "en_US",
type_list: typing.List[str] = ["plain", "html", "image"],
sub_type: str = None,
use_short_email: bool = False,
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
"""
Generates an email including headers with the defined multiparts
:param locale:
:param type_list: options are plain, html, image (attachment), file (attachment), and executable (.exe attachment)
:param sub_type: multipart sub type that defaults to "mixed" if not specified
:param use_short_email: produces a "To" or "From" that is only the email address if True
"""
"""
msg = MIMEMultipart(sub_type) if sub_type else MIMEMultipart()
for part_type in type_list:
msg.attach(generate_mime_part(locale=locale, part_type=part_type))
from_meta, to_meta = add_simple_email_headers(msg, locale=locale, use_short_email=use_short_email)
from_meta, to_meta = add_simple_email_headers(
msg, locale=locale, use_short_email=use_short_email
)
return msg, from_meta, to_meta
def generate_text_email(locale: str="en_US",
use_short_email: bool=False
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
def generate_text_email(
locale: str = "en_US", use_short_email: bool = False
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
"""
Generates an email including headers
"""
body = get_fake("text", locale=locale, min_length=1024)
msg = MIMEText(body)
from_meta, to_meta = add_simple_email_headers(msg, locale=locale, use_short_email=use_short_email)
from_meta, to_meta = add_simple_email_headers(
msg, locale=locale, use_short_email=use_short_email
)
return msg, from_meta, to_meta
def generate_html_email(locale: str="en_US",
use_short_email: bool=False
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
def generate_html_email(
locale: str = "en_US", use_short_email: bool = False
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
"""
Generates an email including headers
"""
body = get_fake_html(locale=locale)
msg = MIMEText(body)
from_meta, to_meta = add_simple_email_headers(msg, locale=locale, use_short_email=use_short_email)
from_meta, to_meta = add_simple_email_headers(
msg, locale=locale, use_short_email=use_short_email
)
return msg, from_meta, to_meta

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

Some files were not shown because too many files have changed in this diff Show More