mirror of
https://github.com/django-helpdesk/django-helpdesk.git
synced 2025-06-04 17:05:36 +02:00
Merge pull request #1249 from DavidVadnais/BUG-1187-Fix-make-check-format
Bug 1187 fix make check format
This commit is contained in:
commit
f3cfd0ec4e
7
.github/workflows/pythonpackage.yml
vendored
7
.github/workflows/pythonpackage.yml
vendored
@ -9,9 +9,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
# Explicitly include Python 3.8 and 3.9 only with Django 4
|
# Explicitly include Python 3.9 only with Django 4
|
||||||
- python-version: "3.8"
|
|
||||||
django-version: "4"
|
|
||||||
- python-version: "3.9"
|
- python-version: "3.9"
|
||||||
django-version: "4"
|
django-version: "4"
|
||||||
# Define the general matrix for Python with Django
|
# Define the general matrix for Python with Django
|
||||||
@ -38,7 +36,8 @@ jobs:
|
|||||||
- name: Lint with ruff
|
- name: Lint with ruff
|
||||||
run: |
|
run: |
|
||||||
pip install ruff
|
pip install ruff
|
||||||
ruff check helpdesk
|
make checkformat
|
||||||
|
|
||||||
|
|
||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: |
|
||||||
|
2
LICENSE
2
LICENSE
@ -1,5 +1,5 @@
|
|||||||
Copyright (c) 2008 Ross Poulton (Trading as Jutda),
|
Copyright (c) 2008 Ross Poulton (Trading as Jutda),
|
||||||
Copyright (c) 2008-2021 django-helpdesk contributors.
|
Copyright (c) 2008-2025 django-helpdesk contributors.
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification,
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
6
Makefile
6
Makefile
@ -63,15 +63,13 @@ test:
|
|||||||
#: format - Run the PEP8 formatter.
|
#: format - Run the PEP8 formatter.
|
||||||
.PHONY: format
|
.PHONY: format
|
||||||
format:
|
format:
|
||||||
autopep8 --exit-code --global-config .flake8 helpdesk
|
ruff format helpdesk
|
||||||
isort --line-length=120 --src helpdesk .
|
|
||||||
|
|
||||||
|
|
||||||
#: checkformat - checks formatting against configured format specifications for the project.
|
#: checkformat - checks formatting against configured format specifications for the project.
|
||||||
.PHONY: checkformat
|
.PHONY: checkformat
|
||||||
checkformat:
|
checkformat:
|
||||||
flake8 helpdesk --count --show-source --statistics --exit-zero --max-complexity=20
|
ruff check helpdesk
|
||||||
isort --line-length=120 --src helpdesk . --check
|
|
||||||
|
|
||||||
|
|
||||||
#: documentation - Build documentation (Sphinx, README, ...).
|
#: documentation - Build documentation (Sphinx, README, ...).
|
||||||
|
@ -8,7 +8,7 @@ django-helpdesk - A Django powered ticket tracker for small businesses.
|
|||||||
.. image:: https://codecov.io/gh/django-helpdesk/django-helpdesk/branch/develop/graph/badge.svg
|
.. image:: https://codecov.io/gh/django-helpdesk/django-helpdesk/branch/develop/graph/badge.svg
|
||||||
:target: https://codecov.io/gh/django-helpdesk/django-helpdesk
|
:target: https://codecov.io/gh/django-helpdesk/django-helpdesk
|
||||||
|
|
||||||
Copyright 2009-2023 Ross Poulton and django-helpdesk contributors. All Rights Reserved.
|
Copyright 2009-2025 Ross Poulton and django-helpdesk contributors. All Rights Reserved.
|
||||||
See LICENSE for details.
|
See LICENSE for details.
|
||||||
|
|
||||||
django-helpdesk was formerly known as Jutda Helpdesk, named after the
|
django-helpdesk was formerly known as Jutda Helpdesk, named after the
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from helpdesk import settings as helpdesk_settings
|
from helpdesk import settings as helpdesk_settings
|
||||||
@ -16,7 +15,7 @@ from helpdesk.models import (
|
|||||||
PreSetReply,
|
PreSetReply,
|
||||||
Queue,
|
Queue,
|
||||||
Ticket,
|
Ticket,
|
||||||
TicketChange
|
TicketChange,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -26,7 +25,7 @@ if helpdesk_settings.HELPDESK_KB_ENABLED:
|
|||||||
|
|
||||||
@admin.register(Queue)
|
@admin.register(Queue)
|
||||||
class QueueAdmin(admin.ModelAdmin):
|
class QueueAdmin(admin.ModelAdmin):
|
||||||
list_display = ('title', 'slug', 'email_address', 'locale', 'time_spent')
|
list_display = ("title", "slug", "email_address", "locale", "time_spent")
|
||||||
prepopulated_fields = {"slug": ("title",)}
|
prepopulated_fields = {"slug": ("title",)}
|
||||||
|
|
||||||
def time_spent(self, q):
|
def time_spent(self, q):
|
||||||
@ -44,11 +43,17 @@ class QueueAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Ticket)
|
@admin.register(Ticket)
|
||||||
class TicketAdmin(admin.ModelAdmin):
|
class TicketAdmin(admin.ModelAdmin):
|
||||||
list_display = ('title', 'status', 'assigned_to', 'queue',
|
list_display = (
|
||||||
'hidden_submitter_email', 'time_spent')
|
"title",
|
||||||
date_hierarchy = 'created'
|
"status",
|
||||||
list_filter = ('queue', 'assigned_to', 'status')
|
"assigned_to",
|
||||||
search_fields = ('id', 'title')
|
"queue",
|
||||||
|
"hidden_submitter_email",
|
||||||
|
"time_spent",
|
||||||
|
)
|
||||||
|
date_hierarchy = "created"
|
||||||
|
list_filter = ("queue", "assigned_to", "status")
|
||||||
|
search_fields = ("id", "title")
|
||||||
|
|
||||||
def hidden_submitter_email(self, ticket):
|
def hidden_submitter_email(self, ticket):
|
||||||
if ticket.submitter_email:
|
if ticket.submitter_email:
|
||||||
@ -58,7 +63,8 @@ class TicketAdmin(admin.ModelAdmin):
|
|||||||
return "%s@%s" % (username, domain)
|
return "%s@%s" % (username, domain)
|
||||||
else:
|
else:
|
||||||
return ticket.submitter_email
|
return ticket.submitter_email
|
||||||
hidden_submitter_email.short_description = _('Submitter E-Mail')
|
|
||||||
|
hidden_submitter_email.short_description = _("Submitter E-Mail")
|
||||||
|
|
||||||
def time_spent(self, ticket):
|
def time_spent(self, ticket):
|
||||||
return ticket.time_spent
|
return ticket.time_spent
|
||||||
@ -82,51 +88,60 @@ class KBIAttachmentInline(admin.StackedInline):
|
|||||||
@admin.register(FollowUp)
|
@admin.register(FollowUp)
|
||||||
class FollowUpAdmin(admin.ModelAdmin):
|
class FollowUpAdmin(admin.ModelAdmin):
|
||||||
inlines = [TicketChangeInline, FollowUpAttachmentInline]
|
inlines = [TicketChangeInline, FollowUpAttachmentInline]
|
||||||
list_display = ('ticket_get_ticket_for_url', 'title', 'date', 'ticket',
|
list_display = (
|
||||||
'user', 'new_status', 'time_spent')
|
"ticket_get_ticket_for_url",
|
||||||
list_filter = ('user', 'date', 'new_status')
|
"title",
|
||||||
|
"date",
|
||||||
|
"ticket",
|
||||||
|
"user",
|
||||||
|
"new_status",
|
||||||
|
"time_spent",
|
||||||
|
)
|
||||||
|
list_filter = ("user", "date", "new_status")
|
||||||
|
|
||||||
def ticket_get_ticket_for_url(self, obj):
|
def ticket_get_ticket_for_url(self, obj):
|
||||||
return obj.ticket.ticket_for_url
|
return obj.ticket.ticket_for_url
|
||||||
ticket_get_ticket_for_url.short_description = _('Slug')
|
|
||||||
|
ticket_get_ticket_for_url.short_description = _("Slug")
|
||||||
|
|
||||||
|
|
||||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||||
|
|
||||||
@admin.register(KBItem)
|
@admin.register(KBItem)
|
||||||
class KBItemAdmin(admin.ModelAdmin):
|
class KBItemAdmin(admin.ModelAdmin):
|
||||||
list_display = ('category', 'title', 'last_updated',
|
list_display = ("category", "title", "last_updated", "team", "order", "enabled")
|
||||||
'team', 'order', 'enabled')
|
|
||||||
inlines = [KBIAttachmentInline]
|
inlines = [KBIAttachmentInline]
|
||||||
readonly_fields = ('voted_by', 'downvoted_by')
|
readonly_fields = ("voted_by", "downvoted_by")
|
||||||
|
|
||||||
list_display_links = ('title',)
|
list_display_links = ("title",)
|
||||||
|
|
||||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||||
|
|
||||||
@admin.register(KBCategory)
|
@admin.register(KBCategory)
|
||||||
class KBCategoryAdmin(admin.ModelAdmin):
|
class KBCategoryAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'title', 'slug', 'public')
|
list_display = ("name", "title", "slug", "public")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(CustomField)
|
@admin.register(CustomField)
|
||||||
class CustomFieldAdmin(admin.ModelAdmin):
|
class CustomFieldAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'label', 'data_type')
|
list_display = ("name", "label", "data_type")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(EmailTemplate)
|
@admin.register(EmailTemplate)
|
||||||
class EmailTemplateAdmin(admin.ModelAdmin):
|
class EmailTemplateAdmin(admin.ModelAdmin):
|
||||||
list_display = ('template_name', 'heading', 'locale')
|
list_display = ("template_name", "heading", "locale")
|
||||||
list_filter = ('locale', )
|
list_filter = ("locale",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(IgnoreEmail)
|
@admin.register(IgnoreEmail)
|
||||||
class IgnoreEmailAdmin(admin.ModelAdmin):
|
class IgnoreEmailAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'queue_list', 'email_address', 'keep_in_mailbox')
|
list_display = ("name", "queue_list", "email_address", "keep_in_mailbox")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ChecklistTemplate)
|
@admin.register(ChecklistTemplate)
|
||||||
class ChecklistTemplateAdmin(admin.ModelAdmin):
|
class ChecklistTemplateAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'task_list')
|
list_display = ("name", "task_list")
|
||||||
search_fields = ('name', 'task_list')
|
search_fields = ("name", "task_list")
|
||||||
|
|
||||||
|
|
||||||
class ChecklistTaskInline(admin.TabularInline):
|
class ChecklistTaskInline(admin.TabularInline):
|
||||||
@ -135,10 +150,10 @@ class ChecklistTaskInline(admin.TabularInline):
|
|||||||
|
|
||||||
@admin.register(Checklist)
|
@admin.register(Checklist)
|
||||||
class ChecklistAdmin(admin.ModelAdmin):
|
class ChecklistAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'ticket')
|
list_display = ("name", "ticket")
|
||||||
search_fields = ('name', 'ticket__id', 'ticket__title')
|
search_fields = ("name", "ticket__id", "ticket__title")
|
||||||
autocomplete_fields = ('ticket',)
|
autocomplete_fields = ("ticket",)
|
||||||
list_select_related = ('ticket',)
|
list_select_related = ("ticket",)
|
||||||
inlines = (ChecklistTaskInline,)
|
inlines = (ChecklistTaskInline,)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,12 +2,12 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class HelpdeskConfig(AppConfig):
|
class HelpdeskConfig(AppConfig):
|
||||||
name = 'helpdesk'
|
name = "helpdesk"
|
||||||
verbose_name = "Helpdesk"
|
verbose_name = "Helpdesk"
|
||||||
# for Django 3.2 support:
|
# for Django 3.2 support:
|
||||||
# see:
|
# see:
|
||||||
# https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.default_auto_field
|
# https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.default_auto_field
|
||||||
default_auto_field = 'django.db.models.AutoField'
|
default_auto_field = "django.db.models.AutoField"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from . import webhooks # noqa: F401
|
from . import webhooks # noqa: F401
|
||||||
|
@ -12,6 +12,7 @@ def check_staff_status(check_staff=False):
|
|||||||
The function most only take one User parameter at the end for use with
|
The function most only take one User parameter at the end for use with
|
||||||
the Django function user_passes_test.
|
the Django function user_passes_test.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def check_superuser_status(check_superuser):
|
def check_superuser_status(check_superuser):
|
||||||
def check_user_status(u):
|
def check_user_status(u):
|
||||||
is_ok = u.is_authenticated and u.is_active
|
is_ok = u.is_authenticated and u.is_active
|
||||||
@ -21,7 +22,9 @@ def check_staff_status(check_staff=False):
|
|||||||
return is_ok and u.is_superuser
|
return is_ok and u.is_superuser
|
||||||
else:
|
else:
|
||||||
return is_ok
|
return is_ok
|
||||||
|
|
||||||
return check_user_status
|
return check_user_status
|
||||||
|
|
||||||
return check_superuser_status
|
return check_superuser_status
|
||||||
|
|
||||||
|
|
||||||
@ -43,11 +46,18 @@ def protect_view(view_func):
|
|||||||
Decorator for protecting the views checking user, redirecting
|
Decorator for protecting the views checking user, redirecting
|
||||||
to the log-in page if necessary or returning 404 status code
|
to the log-in page if necessary or returning 404 status code
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(view_func)
|
@wraps(view_func)
|
||||||
def _wrapped_view(request, *args, **kwargs):
|
def _wrapped_view(request, *args, **kwargs):
|
||||||
if not request.user.is_authenticated and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT:
|
if (
|
||||||
return redirect('helpdesk:login')
|
not request.user.is_authenticated
|
||||||
elif not request.user.is_authenticated and helpdesk_settings.HELPDESK_ANON_ACCESS_RAISES_404:
|
and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT
|
||||||
|
):
|
||||||
|
return redirect("helpdesk:login")
|
||||||
|
elif (
|
||||||
|
not request.user.is_authenticated
|
||||||
|
and helpdesk_settings.HELPDESK_ANON_ACCESS_RAISES_404
|
||||||
|
):
|
||||||
raise Http404
|
raise Http404
|
||||||
if auth_redirect := helpdesk_settings.HELPDESK_PUBLIC_VIEW_PROTECTOR(request):
|
if auth_redirect := helpdesk_settings.HELPDESK_PUBLIC_VIEW_PROTECTOR(request):
|
||||||
return auth_redirect
|
return auth_redirect
|
||||||
@ -61,11 +71,15 @@ def staff_member_required(view_func):
|
|||||||
Decorator for staff member the views checking user, redirecting
|
Decorator for staff member the views checking user, redirecting
|
||||||
to the log-in page if necessary or returning 403
|
to the log-in page if necessary or returning 403
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(view_func)
|
@wraps(view_func)
|
||||||
def _wrapped_view(request, *args, **kwargs):
|
def _wrapped_view(request, *args, **kwargs):
|
||||||
if not request.user.is_authenticated and not request.user.is_active:
|
if not request.user.is_authenticated and not request.user.is_active:
|
||||||
return redirect('helpdesk:login')
|
return redirect("helpdesk:login")
|
||||||
if not helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE and not request.user.is_staff:
|
if (
|
||||||
|
not helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
||||||
|
and not request.user.is_staff
|
||||||
|
):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
if auth_redirect := helpdesk_settings.HELPDESK_STAFF_VIEW_PROTECTOR(request):
|
if auth_redirect := helpdesk_settings.HELPDESK_STAFF_VIEW_PROTECTOR(request):
|
||||||
return auth_redirect
|
return auth_redirect
|
||||||
@ -79,10 +93,11 @@ def superuser_required(view_func):
|
|||||||
Decorator for superuser member the views checking user, redirecting
|
Decorator for superuser member the views checking user, redirecting
|
||||||
to the log-in page if necessary or returning 403
|
to the log-in page if necessary or returning 403
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(view_func)
|
@wraps(view_func)
|
||||||
def _wrapped_view(request, *args, **kwargs):
|
def _wrapped_view(request, *args, **kwargs):
|
||||||
if not request.user.is_authenticated and not request.user.is_active:
|
if not request.user.is_authenticated and not request.user.is_active:
|
||||||
return redirect('helpdesk:login')
|
return redirect("helpdesk:login")
|
||||||
if not request.user.is_superuser:
|
if not request.user.is_superuser:
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
return view_func(request, *args, **kwargs)
|
return view_func(request, *args, **kwargs)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@ class IgnoreTicketException(Exception):
|
|||||||
"""
|
"""
|
||||||
Raised when an email message is received from a sender who is marked to be ignored
|
Raised when an email message is received from a sender who is marked to be ignored
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -10,4 +11,5 @@ class DeleteIgnoredTicketException(Exception):
|
|||||||
Raised when an email message is received from a sender who is marked to be ignored
|
Raised when an email message is received from a sender who is marked to be ignored
|
||||||
and the record is tagged to delete the email from the inbox
|
and the record is tagged to delete the email from the inbox
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
@ -27,7 +27,7 @@ from helpdesk.models import (
|
|||||||
TicketCC,
|
TicketCC,
|
||||||
TicketCustomFieldValue,
|
TicketCustomFieldValue,
|
||||||
TicketDependency,
|
TicketDependency,
|
||||||
UserSettings
|
UserSettings,
|
||||||
)
|
)
|
||||||
from helpdesk.settings import (
|
from helpdesk.settings import (
|
||||||
CUSTOMFIELD_DATE_FORMAT,
|
CUSTOMFIELD_DATE_FORMAT,
|
||||||
@ -55,67 +55,71 @@ class CustomFieldMixin(object):
|
|||||||
|
|
||||||
def customfield_to_field(self, field, instanceargs):
|
def customfield_to_field(self, field, instanceargs):
|
||||||
# Use TextInput widget by default
|
# Use TextInput widget by default
|
||||||
instanceargs['widget'] = forms.TextInput(
|
instanceargs["widget"] = forms.TextInput(attrs={"class": "form-control"})
|
||||||
attrs={'class': 'form-control'})
|
|
||||||
# if-elif branches start with special cases
|
# if-elif branches start with special cases
|
||||||
if field.data_type == 'varchar':
|
if field.data_type == "varchar":
|
||||||
fieldclass = forms.CharField
|
fieldclass = forms.CharField
|
||||||
instanceargs['max_length'] = field.max_length
|
instanceargs["max_length"] = field.max_length
|
||||||
elif field.data_type == 'text':
|
elif field.data_type == "text":
|
||||||
fieldclass = forms.CharField
|
fieldclass = forms.CharField
|
||||||
instanceargs['widget'] = forms.Textarea(
|
instanceargs["widget"] = forms.Textarea(attrs={"class": "form-control"})
|
||||||
attrs={'class': 'form-control'})
|
instanceargs["max_length"] = field.max_length
|
||||||
instanceargs['max_length'] = field.max_length
|
elif field.data_type == "integer":
|
||||||
elif field.data_type == 'integer':
|
|
||||||
fieldclass = forms.IntegerField
|
fieldclass = forms.IntegerField
|
||||||
instanceargs['widget'] = forms.NumberInput(
|
instanceargs["widget"] = forms.NumberInput(attrs={"class": "form-control"})
|
||||||
attrs={'class': 'form-control'})
|
elif field.data_type == "decimal":
|
||||||
elif field.data_type == 'decimal':
|
|
||||||
fieldclass = forms.DecimalField
|
fieldclass = forms.DecimalField
|
||||||
instanceargs['decimal_places'] = field.decimal_places
|
instanceargs["decimal_places"] = field.decimal_places
|
||||||
instanceargs['max_digits'] = field.max_length
|
instanceargs["max_digits"] = field.max_length
|
||||||
instanceargs['widget'] = forms.NumberInput(
|
instanceargs["widget"] = forms.NumberInput(attrs={"class": "form-control"})
|
||||||
attrs={'class': 'form-control'})
|
elif field.data_type == "list":
|
||||||
elif field.data_type == 'list':
|
|
||||||
fieldclass = forms.ChoiceField
|
fieldclass = forms.ChoiceField
|
||||||
instanceargs['choices'] = field.get_choices()
|
instanceargs["choices"] = field.get_choices()
|
||||||
instanceargs['widget'] = forms.Select(
|
instanceargs["widget"] = forms.Select(attrs={"class": "form-control"})
|
||||||
attrs={'class': 'form-control'})
|
|
||||||
else:
|
else:
|
||||||
# Try to use the immediate equivalences dictionary
|
# Try to use the immediate equivalences dictionary
|
||||||
try:
|
try:
|
||||||
fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type]
|
fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type]
|
||||||
# Change widgets for the following classes
|
# Change widgets for the following classes
|
||||||
if fieldclass == forms.DateField:
|
if fieldclass == forms.DateField:
|
||||||
instanceargs['widget'] = forms.DateInput(
|
instanceargs["widget"] = forms.DateInput(
|
||||||
attrs={'class': 'form-control date-field'})
|
attrs={"class": "form-control date-field"}
|
||||||
|
)
|
||||||
elif fieldclass == forms.DateTimeField:
|
elif fieldclass == forms.DateTimeField:
|
||||||
instanceargs['widget'] = forms.DateTimeInput(
|
instanceargs["widget"] = forms.DateTimeInput(
|
||||||
attrs={'class': 'form-control datetime-field'})
|
attrs={"class": "form-control datetime-field"}
|
||||||
|
)
|
||||||
elif fieldclass == forms.TimeField:
|
elif fieldclass == forms.TimeField:
|
||||||
instanceargs['widget'] = forms.TimeInput(
|
instanceargs["widget"] = forms.TimeInput(
|
||||||
attrs={'class': 'form-control time-field'})
|
attrs={"class": "form-control time-field"}
|
||||||
|
)
|
||||||
elif fieldclass == forms.BooleanField:
|
elif fieldclass == forms.BooleanField:
|
||||||
instanceargs['widget'] = forms.CheckboxInput(
|
instanceargs["widget"] = forms.CheckboxInput(
|
||||||
attrs={'class': 'form-control'})
|
attrs={"class": "form-control"}
|
||||||
|
)
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# The data_type was not found anywhere
|
# The data_type was not found anywhere
|
||||||
raise NameError("Unrecognized data_type %s" % field.data_type)
|
raise NameError("Unrecognized data_type %s" % field.data_type)
|
||||||
|
|
||||||
self.fields['custom_%s' % field.name] = fieldclass(**instanceargs)
|
self.fields["custom_%s" % field.name] = fieldclass(**instanceargs)
|
||||||
|
|
||||||
|
|
||||||
class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Ticket
|
model = Ticket
|
||||||
exclude = ('created', 'modified', 'status', 'on_hold',
|
exclude = (
|
||||||
'resolution', 'last_escalation', 'assigned_to')
|
"created",
|
||||||
|
"modified",
|
||||||
|
"status",
|
||||||
|
"on_hold",
|
||||||
|
"resolution",
|
||||||
|
"last_escalation",
|
||||||
|
"assigned_to",
|
||||||
|
)
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
js = ('helpdesk/js/init_due_date.js',
|
js = ("helpdesk/js/init_due_date.js", "helpdesk/js/init_datetime_classes.js")
|
||||||
'helpdesk/js/init_datetime_classes.js')
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -124,56 +128,62 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
|||||||
super(EditTicketForm, self).__init__(*args, **kwargs)
|
super(EditTicketForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Disable and add help_text to the merged_to field on this form
|
# Disable and add help_text to the merged_to field on this form
|
||||||
self.fields['merged_to'].disabled = True
|
self.fields["merged_to"].disabled = True
|
||||||
self.fields['merged_to'].help_text = _(
|
self.fields["merged_to"].help_text = _(
|
||||||
'This ticket is merged into the selected ticket.')
|
"This ticket is merged into the selected ticket."
|
||||||
|
)
|
||||||
|
|
||||||
for field in CustomField.objects.all():
|
for field in CustomField.objects.all():
|
||||||
initial_value = None
|
initial_value = None
|
||||||
try:
|
try:
|
||||||
current_value = TicketCustomFieldValue.objects.get(
|
current_value = TicketCustomFieldValue.objects.get(
|
||||||
ticket=self.instance, field=field)
|
ticket=self.instance, field=field
|
||||||
|
)
|
||||||
initial_value = current_value.value
|
initial_value = current_value.value
|
||||||
# Attempt to convert from fixed format string to date/time data
|
# Attempt to convert from fixed format string to date/time data
|
||||||
# type
|
# type
|
||||||
if 'datetime' == current_value.field.data_type:
|
if "datetime" == current_value.field.data_type:
|
||||||
initial_value = datetime.strptime(
|
initial_value = datetime.strptime(
|
||||||
initial_value, CUSTOMFIELD_DATETIME_FORMAT)
|
initial_value, CUSTOMFIELD_DATETIME_FORMAT
|
||||||
elif 'date' == current_value.field.data_type:
|
)
|
||||||
|
elif "date" == current_value.field.data_type:
|
||||||
initial_value = datetime.strptime(
|
initial_value = datetime.strptime(
|
||||||
initial_value, CUSTOMFIELD_DATE_FORMAT)
|
initial_value, CUSTOMFIELD_DATE_FORMAT
|
||||||
elif 'time' == current_value.field.data_type:
|
)
|
||||||
|
elif "time" == current_value.field.data_type:
|
||||||
initial_value = datetime.strptime(
|
initial_value = datetime.strptime(
|
||||||
initial_value, CUSTOMFIELD_TIME_FORMAT)
|
initial_value, CUSTOMFIELD_TIME_FORMAT
|
||||||
|
)
|
||||||
# If it is boolean field, transform the value to a real boolean
|
# If it is boolean field, transform the value to a real boolean
|
||||||
# instead of a string
|
# instead of a string
|
||||||
elif 'boolean' == current_value.field.data_type:
|
elif "boolean" == current_value.field.data_type:
|
||||||
initial_value = 'True' == initial_value
|
initial_value = "True" == initial_value
|
||||||
except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError):
|
except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError):
|
||||||
# ValueError error if parsing fails, using initial_value = current_value.value
|
# ValueError error if parsing fails, using initial_value = current_value.value
|
||||||
# TypeError if parsing None type
|
# TypeError if parsing None type
|
||||||
pass
|
pass
|
||||||
instanceargs = {
|
instanceargs = {
|
||||||
'label': field.label,
|
"label": field.label,
|
||||||
'help_text': field.help_text,
|
"help_text": field.help_text,
|
||||||
'required': field.required,
|
"required": field.required,
|
||||||
'initial': initial_value,
|
"initial": initial_value,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.customfield_to_field(field, instanceargs)
|
self.customfield_to_field(field, instanceargs)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
for field, value in self.cleaned_data.items():
|
for field, value in self.cleaned_data.items():
|
||||||
if field.startswith('custom_'):
|
if field.startswith("custom_"):
|
||||||
field_name = field.replace('custom_', '', 1)
|
field_name = field.replace("custom_", "", 1)
|
||||||
customfield = CustomField.objects.get(name=field_name)
|
customfield = CustomField.objects.get(name=field_name)
|
||||||
try:
|
try:
|
||||||
cfv = TicketCustomFieldValue.objects.get(
|
cfv = TicketCustomFieldValue.objects.get(
|
||||||
ticket=self.instance, field=customfield)
|
ticket=self.instance, field=customfield
|
||||||
|
)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
cfv = TicketCustomFieldValue(
|
cfv = TicketCustomFieldValue(
|
||||||
ticket=self.instance, field=customfield)
|
ticket=self.instance, field=customfield
|
||||||
|
)
|
||||||
|
|
||||||
cfv.value = convert_value(value)
|
cfv.value = convert_value(value)
|
||||||
cfv.save()
|
cfv.save()
|
||||||
@ -195,21 +205,24 @@ class EditTicketCustomFieldForm(EditTicketForm):
|
|||||||
if HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST:
|
if HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST:
|
||||||
fields = list(self.fields)
|
fields = list(self.fields)
|
||||||
for field in fields:
|
for field in fields:
|
||||||
if field != 'id' and field.replace("custom_", "", 1) not in HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST:
|
if (
|
||||||
|
field != "id"
|
||||||
|
and field.replace("custom_", "", 1)
|
||||||
|
not in HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST
|
||||||
|
):
|
||||||
self.fields.pop(field, None)
|
self.fields.pop(field, None)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
# if form is saved in a ticket update, it is passed
|
# if form is saved in a ticket update, it is passed
|
||||||
# a followup instance to trace custom fields changes
|
# a followup instance to trace custom fields changes
|
||||||
if "followup" in kwargs:
|
if "followup" in kwargs:
|
||||||
followup = kwargs.pop('followup', None)
|
followup = kwargs.pop("followup", None)
|
||||||
|
|
||||||
for field, value in self.cleaned_data.items():
|
for field, value in self.cleaned_data.items():
|
||||||
if field.startswith('custom_'):
|
if field.startswith("custom_"):
|
||||||
if value != self.fields[field].initial:
|
if value != self.fields[field].initial:
|
||||||
c = followup.ticketchange_set.create(
|
c = followup.ticketchange_set.create(
|
||||||
field=field.replace('custom_', '', 1),
|
field=field.replace("custom_", "", 1),
|
||||||
old_value=self.fields[field].initial,
|
old_value=self.fields[field].initial,
|
||||||
new_value=value,
|
new_value=value,
|
||||||
)
|
)
|
||||||
@ -218,20 +231,26 @@ class EditTicketCustomFieldForm(EditTicketForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Ticket
|
model = Ticket
|
||||||
fields = ('id', 'merged_to',)
|
fields = (
|
||||||
|
"id",
|
||||||
|
"merged_to",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EditFollowUpForm(forms.ModelForm):
|
class EditFollowUpForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FollowUp
|
model = FollowUp
|
||||||
exclude = ('date', 'user',)
|
exclude = (
|
||||||
|
"date",
|
||||||
|
"user",
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Filter not opened tickets here."""
|
"""Filter not opened tickets here."""
|
||||||
super(EditFollowUpForm, self).__init__(*args, **kwargs)
|
super(EditFollowUpForm, self).__init__(*args, **kwargs)
|
||||||
self.fields["ticket"].queryset = Ticket.objects.filter(
|
self.fields["ticket"].queryset = Ticket.objects.filter(
|
||||||
status__in=Ticket.OPEN_STATUSES)
|
status__in=Ticket.OPEN_STATUSES
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||||
@ -239,73 +258,81 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
|||||||
Contain all the common code and fields between "TicketForm" and
|
Contain all the common code and fields between "TicketForm" and
|
||||||
"PublicTicketForm". This Form is not intended to be used directly.
|
"PublicTicketForm". This Form is not intended to be used directly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queue = forms.ChoiceField(
|
queue = forms.ChoiceField(
|
||||||
widget=forms.Select(attrs={'class': 'form-control'}),
|
widget=forms.Select(attrs={"class": "form-control"}),
|
||||||
label=_('Queue'),
|
label=_("Queue"),
|
||||||
required=True,
|
required=True,
|
||||||
choices=()
|
choices=(),
|
||||||
)
|
)
|
||||||
|
|
||||||
title = forms.CharField(
|
title = forms.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
required=True,
|
required=True,
|
||||||
widget=forms.TextInput(attrs={'class': 'form-control'}),
|
widget=forms.TextInput(attrs={"class": "form-control"}),
|
||||||
label=_('Summary of the problem'),
|
label=_("Summary of the problem"),
|
||||||
)
|
)
|
||||||
|
|
||||||
body = forms.CharField(
|
body = forms.CharField(
|
||||||
widget=forms.Textarea(attrs={'class': 'form-control'}),
|
widget=forms.Textarea(attrs={"class": "form-control"}),
|
||||||
label=_('Description of your issue'),
|
label=_("Description of your issue"),
|
||||||
required=True,
|
required=True,
|
||||||
help_text=_(
|
help_text=_("Please be as descriptive as possible and include all details"),
|
||||||
'Please be as descriptive as possible and include all details'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
priority = forms.ChoiceField(
|
priority = forms.ChoiceField(
|
||||||
widget=forms.Select(attrs={'class': 'form-control'}),
|
widget=forms.Select(attrs={"class": "form-control"}),
|
||||||
choices=Ticket.PRIORITY_CHOICES,
|
choices=Ticket.PRIORITY_CHOICES,
|
||||||
required=True,
|
required=True,
|
||||||
initial=getattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY', '3'),
|
initial=getattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY", "3"),
|
||||||
label=_('Priority'),
|
label=_("Priority"),
|
||||||
help_text=_(
|
help_text=_("Please select a priority carefully. If unsure, leave it as '3'."),
|
||||||
"Please select a priority carefully. If unsure, leave it as '3'."),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
due_date = forms.DateTimeField(
|
due_date = forms.DateTimeField(
|
||||||
widget=forms.TextInput(
|
widget=forms.TextInput(attrs={"class": "form-control", "autocomplete": "off"}),
|
||||||
attrs={'class': 'form-control', 'autocomplete': 'off'}),
|
|
||||||
required=False,
|
required=False,
|
||||||
input_formats=[CUSTOMFIELD_DATE_FORMAT,
|
input_formats=[
|
||||||
CUSTOMFIELD_DATETIME_FORMAT, '%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"],
|
CUSTOMFIELD_DATE_FORMAT,
|
||||||
label=_('Due on'),
|
CUSTOMFIELD_DATETIME_FORMAT,
|
||||||
|
"%d/%m/%Y",
|
||||||
|
"%m/%d/%Y",
|
||||||
|
"%d.%m.%Y",
|
||||||
|
],
|
||||||
|
label=_("Due on"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if helpdesk_settings.HELPDESK_ENABLE_ATTACHMENTS:
|
if helpdesk_settings.HELPDESK_ENABLE_ATTACHMENTS:
|
||||||
attachment = forms.FileField(
|
attachment = forms.FileField(
|
||||||
widget=forms.FileInput(attrs={'class': 'form-control-file'}),
|
widget=forms.FileInput(attrs={"class": "form-control-file"}),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Attach File'),
|
label=_("Attach File"),
|
||||||
help_text=_('You can attach a file to this ticket. '
|
help_text=_(
|
||||||
'Only file types such as plain text (.txt), '
|
"You can attach a file to this ticket. "
|
||||||
'a document (.pdf, .docx, or .odt), '
|
"Only file types such as plain text (.txt), "
|
||||||
'or screenshot (.png or .jpg) may be uploaded.'),
|
"a document (.pdf, .docx, or .odt), "
|
||||||
validators=[validate_file_extension]
|
"or screenshot (.png or .jpg) may be uploaded."
|
||||||
|
),
|
||||||
|
validators=[validate_file_extension],
|
||||||
)
|
)
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
js = ('helpdesk/js/init_due_date.js',
|
js = ("helpdesk/js/init_due_date.js", "helpdesk/js/init_datetime_classes.js")
|
||||||
'helpdesk/js/init_datetime_classes.js')
|
|
||||||
|
|
||||||
def __init__(self, kbcategory=None, *args, **kwargs):
|
def __init__(self, kbcategory=None, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||||
if kbcategory:
|
if kbcategory:
|
||||||
self.fields['kbitem'] = forms.ChoiceField(
|
self.fields["kbitem"] = forms.ChoiceField(
|
||||||
widget=forms.Select(attrs={'class': 'form-control'}),
|
widget=forms.Select(attrs={"class": "form-control"}),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Knowledge Base Item'),
|
label=_("Knowledge Base Item"),
|
||||||
choices=[(kbi.pk, kbi.title) for kbi in KBItem.objects.filter(
|
choices=[
|
||||||
category=kbcategory.pk, enabled=True)],
|
(kbi.pk, kbi.title)
|
||||||
|
for kbi in KBItem.objects.filter(
|
||||||
|
category=kbcategory.pk, enabled=True
|
||||||
|
)
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def _add_form_custom_fields(self, staff_only_filter=None):
|
def _add_form_custom_fields(self, staff_only_filter=None):
|
||||||
@ -316,38 +343,37 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
|||||||
|
|
||||||
for field in queryset:
|
for field in queryset:
|
||||||
instanceargs = {
|
instanceargs = {
|
||||||
'label': field.label,
|
"label": field.label,
|
||||||
'help_text': field.help_text,
|
"help_text": field.help_text,
|
||||||
'required': field.required,
|
"required": field.required,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.customfield_to_field(field, instanceargs)
|
self.customfield_to_field(field, instanceargs)
|
||||||
|
|
||||||
def _get_queue(self):
|
def _get_queue(self):
|
||||||
# this procedure is re-defined for public submission form
|
# this procedure is re-defined for public submission form
|
||||||
return Queue.objects.get(id=int(self.cleaned_data['queue']))
|
return Queue.objects.get(id=int(self.cleaned_data["queue"]))
|
||||||
|
|
||||||
def _create_ticket(self):
|
def _create_ticket(self):
|
||||||
queue = self._get_queue()
|
queue = self._get_queue()
|
||||||
kbitem = None
|
kbitem = None
|
||||||
if 'kbitem' in self.cleaned_data:
|
if "kbitem" in self.cleaned_data:
|
||||||
kbitem = KBItem.objects.get(id=int(self.cleaned_data['kbitem']))
|
kbitem = KBItem.objects.get(id=int(self.cleaned_data["kbitem"]))
|
||||||
|
|
||||||
ticket = Ticket(
|
ticket = Ticket(
|
||||||
title=self.cleaned_data['title'],
|
title=self.cleaned_data["title"],
|
||||||
submitter_email=self.cleaned_data['submitter_email'],
|
submitter_email=self.cleaned_data["submitter_email"],
|
||||||
created=timezone.now(),
|
created=timezone.now(),
|
||||||
status=Ticket.OPEN_STATUS,
|
status=Ticket.OPEN_STATUS,
|
||||||
queue=queue,
|
queue=queue,
|
||||||
description=self.cleaned_data['body'],
|
description=self.cleaned_data["body"],
|
||||||
priority=self.cleaned_data.get(
|
priority=self.cleaned_data.get(
|
||||||
'priority',
|
"priority", getattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY", "3")
|
||||||
getattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY", "3")
|
|
||||||
),
|
),
|
||||||
due_date=self.cleaned_data.get(
|
due_date=self.cleaned_data.get(
|
||||||
'due_date',
|
"due_date", getattr(settings, "HELPDESK_PUBLIC_TICKET_DUE_DATE", None)
|
||||||
getattr(settings, "HELPDESK_PUBLIC_TICKET_DUE_DATE", None)
|
)
|
||||||
) or None,
|
or None,
|
||||||
kbitem=kbitem,
|
kbitem=kbitem,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -357,18 +383,19 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
|||||||
ticket.save_custom_field_values(self.cleaned_data)
|
ticket.save_custom_field_values(self.cleaned_data)
|
||||||
|
|
||||||
def _create_follow_up(self, ticket, title, user=None):
|
def _create_follow_up(self, ticket, title, user=None):
|
||||||
followup = FollowUp(ticket=ticket,
|
followup = FollowUp(
|
||||||
title=title,
|
ticket=ticket,
|
||||||
date=timezone.now(),
|
title=title,
|
||||||
public=True,
|
date=timezone.now(),
|
||||||
comment=self.cleaned_data['body'],
|
public=True,
|
||||||
)
|
comment=self.cleaned_data["body"],
|
||||||
|
)
|
||||||
if user:
|
if user:
|
||||||
followup.user = user
|
followup.user = user
|
||||||
return followup
|
return followup
|
||||||
|
|
||||||
def _attach_files_to_follow_up(self, followup):
|
def _attach_files_to_follow_up(self, followup):
|
||||||
files = self.cleaned_data.get('attachment')
|
files = self.cleaned_data.get("attachment")
|
||||||
if files:
|
if files:
|
||||||
files = process_attachments(followup, [files])
|
files = process_attachments(followup, [files])
|
||||||
return files
|
return files
|
||||||
@ -376,13 +403,18 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _send_messages(ticket, queue, followup, files, user=None):
|
def _send_messages(ticket, queue, followup, files, user=None):
|
||||||
context = safe_template_context(ticket)
|
context = safe_template_context(ticket)
|
||||||
context['comment'] = followup.comment
|
context["comment"] = followup.comment
|
||||||
|
|
||||||
roles = {'submitter': ('newticket_submitter', context),
|
roles = {
|
||||||
'new_ticket_cc': ('newticket_cc', context),
|
"submitter": ("newticket_submitter", context),
|
||||||
'ticket_cc': ('newticket_cc', context)}
|
"new_ticket_cc": ("newticket_cc", context),
|
||||||
if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign:
|
"ticket_cc": ("newticket_cc", context),
|
||||||
roles['assigned_to'] = ('assigned_owner', context)
|
}
|
||||||
|
if (
|
||||||
|
ticket.assigned_to
|
||||||
|
and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign
|
||||||
|
):
|
||||||
|
roles["assigned_to"] = ("assigned_owner", context)
|
||||||
ticket.send(
|
ticket.send(
|
||||||
roles,
|
roles,
|
||||||
fail_silently=True,
|
fail_silently=True,
|
||||||
@ -394,26 +426,29 @@ class TicketForm(AbstractTicketForm):
|
|||||||
"""
|
"""
|
||||||
Ticket Form creation for registered users.
|
Ticket Form creation for registered users.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
submitter_email = forms.EmailField(
|
submitter_email = forms.EmailField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Submitter E-Mail Address'),
|
label=_("Submitter E-Mail Address"),
|
||||||
widget=forms.TextInput(
|
widget=forms.TextInput(attrs={"class": "form-control", "type": "email"}),
|
||||||
attrs={'class': 'form-control', 'type': 'email'}),
|
help_text=_(
|
||||||
help_text=_('This e-mail address will receive copies of all public '
|
"This e-mail address will receive copies of all public "
|
||||||
'updates to this ticket.'),
|
"updates to this ticket."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
assigned_to = forms.ChoiceField(
|
assigned_to = forms.ChoiceField(
|
||||||
widget=(
|
widget=(
|
||||||
forms.Select(attrs={'class': 'form-control'})
|
forms.Select(attrs={"class": "form-control"})
|
||||||
if not helpdesk_settings.HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO
|
if not helpdesk_settings.HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO
|
||||||
else forms.HiddenInput()
|
else forms.HiddenInput()
|
||||||
),
|
),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Case owner'),
|
label=_("Case owner"),
|
||||||
help_text=_('If you select an owner other than yourself, they\'ll be '
|
help_text=_(
|
||||||
'e-mailed details of this ticket immediately.'),
|
"If you select an owner other than yourself, they'll be "
|
||||||
|
"e-mailed details of this ticket immediately."
|
||||||
choices=()
|
),
|
||||||
|
choices=(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -424,15 +459,18 @@ class TicketForm(AbstractTicketForm):
|
|||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['queue'].choices = queue_choices
|
self.fields["queue"].choices = queue_choices
|
||||||
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS:
|
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS:
|
||||||
assignable_users = User.objects.filter(
|
assignable_users = User.objects.filter(
|
||||||
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
|
is_active=True, is_staff=True
|
||||||
|
).order_by(User.USERNAME_FIELD)
|
||||||
else:
|
else:
|
||||||
assignable_users = User.objects.filter(
|
assignable_users = User.objects.filter(is_active=True).order_by(
|
||||||
is_active=True).order_by(User.USERNAME_FIELD)
|
User.USERNAME_FIELD
|
||||||
self.fields['assigned_to'].choices = [
|
)
|
||||||
('', '--------')] + [(u.id, u.get_username()) for u in assignable_users]
|
self.fields["assigned_to"].choices = [("", "--------")] + [
|
||||||
|
(u.id, u.get_username()) for u in assignable_users
|
||||||
|
]
|
||||||
self._add_form_custom_fields()
|
self._add_form_custom_fields()
|
||||||
|
|
||||||
def save(self, user):
|
def save(self, user):
|
||||||
@ -441,9 +479,9 @@ class TicketForm(AbstractTicketForm):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
ticket, queue = self._create_ticket()
|
ticket, queue = self._create_ticket()
|
||||||
if self.cleaned_data['assigned_to']:
|
if self.cleaned_data["assigned_to"]:
|
||||||
try:
|
try:
|
||||||
u = User.objects.get(id=self.cleaned_data['assigned_to'])
|
u = User.objects.get(id=self.cleaned_data["assigned_to"])
|
||||||
ticket.assigned_to = u
|
ticket.assigned_to = u
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
ticket.assigned_to = None
|
ticket.assigned_to = None
|
||||||
@ -451,12 +489,12 @@ class TicketForm(AbstractTicketForm):
|
|||||||
|
|
||||||
self._create_custom_fields(ticket)
|
self._create_custom_fields(ticket)
|
||||||
|
|
||||||
if self.cleaned_data['assigned_to']:
|
if self.cleaned_data["assigned_to"]:
|
||||||
title = _('Ticket Opened & Assigned to %(name)s') % {
|
title = _("Ticket Opened & Assigned to %(name)s") % {
|
||||||
'name': ticket.get_assigned_to or _("<invalid user>")
|
"name": ticket.get_assigned_to or _("<invalid user>")
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
title = _('Ticket Opened')
|
title = _("Ticket Opened")
|
||||||
followup = self._create_follow_up(ticket, title=title, user=user)
|
followup = self._create_follow_up(ticket, title=title, user=user)
|
||||||
followup.save()
|
followup.save()
|
||||||
|
|
||||||
@ -468,11 +506,9 @@ class TicketForm(AbstractTicketForm):
|
|||||||
# emit signal when the TicketForm.save is done
|
# emit signal when the TicketForm.save is done
|
||||||
new_ticket_done.send(sender="TicketForm", ticket=ticket)
|
new_ticket_done.send(sender="TicketForm", ticket=ticket)
|
||||||
|
|
||||||
self._send_messages(ticket=ticket,
|
self._send_messages(
|
||||||
queue=queue,
|
ticket=ticket, queue=queue, followup=followup, files=files, user=user
|
||||||
followup=followup,
|
)
|
||||||
files=files,
|
|
||||||
user=user)
|
|
||||||
return ticket
|
return ticket
|
||||||
|
|
||||||
|
|
||||||
@ -480,12 +516,12 @@ class PublicTicketForm(AbstractTicketForm):
|
|||||||
"""
|
"""
|
||||||
Ticket Form creation for all users (public-facing).
|
Ticket Form creation for all users (public-facing).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
submitter_email = forms.EmailField(
|
submitter_email = forms.EmailField(
|
||||||
widget=forms.TextInput(
|
widget=forms.TextInput(attrs={"class": "form-control", "type": "email"}),
|
||||||
attrs={'class': 'form-control', 'type': 'email'}),
|
|
||||||
required=True,
|
required=True,
|
||||||
label=_('Your E-Mail Address'),
|
label=_("Your E-Mail Address"),
|
||||||
help_text=_('We will e-mail you when your ticket is updated.'),
|
help_text=_("We will e-mail you when your ticket is updated."),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, hidden_fields=(), readonly_fields=(), *args, **kwargs):
|
def __init__(self, hidden_fields=(), readonly_fields=(), *args, **kwargs):
|
||||||
@ -502,14 +538,13 @@ class PublicTicketForm(AbstractTicketForm):
|
|||||||
self.fields[field].disabled = True
|
self.fields[field].disabled = True
|
||||||
|
|
||||||
field_deletion_table = {
|
field_deletion_table = {
|
||||||
'queue': 'HELPDESK_PUBLIC_TICKET_QUEUE',
|
"queue": "HELPDESK_PUBLIC_TICKET_QUEUE",
|
||||||
'priority': 'HELPDESK_PUBLIC_TICKET_PRIORITY',
|
"priority": "HELPDESK_PUBLIC_TICKET_PRIORITY",
|
||||||
'due_date': 'HELPDESK_PUBLIC_TICKET_DUE_DATE',
|
"due_date": "HELPDESK_PUBLIC_TICKET_DUE_DATE",
|
||||||
}
|
}
|
||||||
|
|
||||||
for field_name, field_setting_key in field_deletion_table.items():
|
for field_name, field_setting_key in field_deletion_table.items():
|
||||||
has_settings_default_value = getattr(
|
has_settings_default_value = getattr(settings, field_setting_key, None)
|
||||||
settings, field_setting_key, None)
|
|
||||||
if has_settings_default_value is not None:
|
if has_settings_default_value is not None:
|
||||||
del self.fields[field_name]
|
del self.fields[field_name]
|
||||||
|
|
||||||
@ -520,12 +555,13 @@ class PublicTicketForm(AbstractTicketForm):
|
|||||||
"There are no public queues defined - public ticket creation is impossible"
|
"There are no public queues defined - public ticket creation is impossible"
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'queue' in self.fields:
|
if "queue" in self.fields:
|
||||||
self.fields['queue'].choices = [('', '--------')] + [
|
self.fields["queue"].choices = [("", "--------")] + [
|
||||||
(q.id, q.title) for q in public_queues]
|
(q.id, q.title) for q in public_queues
|
||||||
|
]
|
||||||
|
|
||||||
def _get_queue(self):
|
def _get_queue(self):
|
||||||
if getattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE', None) is not None:
|
if getattr(settings, "HELPDESK_PUBLIC_TICKET_QUEUE", None) is not None:
|
||||||
# force queue to be the pre-defined one
|
# force queue to be the pre-defined one
|
||||||
# (only for public submissions)
|
# (only for public submissions)
|
||||||
public_queue = Queue.objects.filter(
|
public_queue = Queue.objects.filter(
|
||||||
@ -534,12 +570,12 @@ class PublicTicketForm(AbstractTicketForm):
|
|||||||
if not public_queue:
|
if not public_queue:
|
||||||
logger.fatal(
|
logger.fatal(
|
||||||
"Public queue '%s' is configured as default but can't be found",
|
"Public queue '%s' is configured as default but can't be found",
|
||||||
settings.HELPDESK_PUBLIC_TICKET_QUEUE
|
settings.HELPDESK_PUBLIC_TICKET_QUEUE,
|
||||||
)
|
)
|
||||||
return public_queue
|
return public_queue
|
||||||
else:
|
else:
|
||||||
# get the queue user entered
|
# get the queue user entered
|
||||||
return Queue.objects.get(id=int(self.cleaned_data['queue']))
|
return Queue.objects.get(id=int(self.cleaned_data["queue"]))
|
||||||
|
|
||||||
def save(self, user):
|
def save(self, user):
|
||||||
"""
|
"""
|
||||||
@ -553,7 +589,8 @@ class PublicTicketForm(AbstractTicketForm):
|
|||||||
self._create_custom_fields(ticket)
|
self._create_custom_fields(ticket)
|
||||||
|
|
||||||
followup = self._create_follow_up(
|
followup = self._create_follow_up(
|
||||||
ticket, title=_('Ticket Opened Via Web'), user=user)
|
ticket, title=_("Ticket Opened Via Web"), user=user
|
||||||
|
)
|
||||||
followup.save()
|
followup.save()
|
||||||
|
|
||||||
files = self._attach_files_to_follow_up(followup)
|
files = self._attach_files_to_follow_up(followup)
|
||||||
@ -561,161 +598,174 @@ class PublicTicketForm(AbstractTicketForm):
|
|||||||
# emit signal when the PublicTicketForm.save is done
|
# emit signal when the PublicTicketForm.save is done
|
||||||
new_ticket_done.send(sender="PublicTicketForm", ticket=ticket)
|
new_ticket_done.send(sender="PublicTicketForm", ticket=ticket)
|
||||||
|
|
||||||
self._send_messages(ticket=ticket,
|
self._send_messages(ticket=ticket, queue=queue, followup=followup, files=files)
|
||||||
queue=queue,
|
|
||||||
followup=followup,
|
|
||||||
files=files)
|
|
||||||
return ticket
|
return ticket
|
||||||
|
|
||||||
|
|
||||||
class UserSettingsForm(forms.ModelForm):
|
class UserSettingsForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UserSettings
|
model = UserSettings
|
||||||
exclude = ['user', 'settings_pickled']
|
exclude = ["user", "settings_pickled"]
|
||||||
|
|
||||||
|
|
||||||
class EmailIgnoreForm(forms.ModelForm):
|
class EmailIgnoreForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IgnoreEmail
|
model = IgnoreEmail
|
||||||
exclude = []
|
exclude = []
|
||||||
|
|
||||||
|
|
||||||
class TicketCCForm(forms.ModelForm):
|
class TicketCCForm(forms.ModelForm):
|
||||||
''' Adds either an email address or helpdesk user as a CC on a Ticket. Used for processing POST requests. '''
|
"""Adds either an email address or helpdesk user as a CC on a Ticket. Used for processing POST requests."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TicketCC
|
model = TicketCC
|
||||||
exclude = ('ticket',)
|
exclude = ("ticket",)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(TicketCCForm, self).__init__(*args, **kwargs)
|
super(TicketCCForm, self).__init__(*args, **kwargs)
|
||||||
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
|
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
|
||||||
users = User.objects.filter(
|
users = User.objects.filter(is_active=True, is_staff=True).order_by(
|
||||||
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
|
User.USERNAME_FIELD
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
users = User.objects.filter(
|
users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD)
|
||||||
is_active=True).order_by(User.USERNAME_FIELD)
|
self.fields["user"].queryset = users
|
||||||
self.fields['user'].queryset = users
|
|
||||||
|
|
||||||
|
|
||||||
class TicketCCUserForm(forms.ModelForm):
|
class TicketCCUserForm(forms.ModelForm):
|
||||||
''' Adds a helpdesk user as a CC on a Ticket '''
|
"""Adds a helpdesk user as a CC on a Ticket"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(TicketCCUserForm, self).__init__(*args, **kwargs)
|
super(TicketCCUserForm, self).__init__(*args, **kwargs)
|
||||||
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
|
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
|
||||||
users = User.objects.filter(
|
users = User.objects.filter(is_active=True, is_staff=True).order_by(
|
||||||
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
|
User.USERNAME_FIELD
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
users = User.objects.filter(
|
users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD)
|
||||||
is_active=True).order_by(User.USERNAME_FIELD)
|
self.fields["user"].queryset = users
|
||||||
self.fields['user'].queryset = users
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TicketCC
|
model = TicketCC
|
||||||
exclude = ('ticket', 'email',)
|
exclude = (
|
||||||
|
"ticket",
|
||||||
|
"email",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TicketCCEmailForm(forms.ModelForm):
|
class TicketCCEmailForm(forms.ModelForm):
|
||||||
''' Adds an email address as a CC on a Ticket '''
|
"""Adds an email address as a CC on a Ticket"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(TicketCCEmailForm, self).__init__(*args, **kwargs)
|
super(TicketCCEmailForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TicketCC
|
model = TicketCC
|
||||||
exclude = ('ticket', 'user',)
|
exclude = (
|
||||||
|
"ticket",
|
||||||
|
"user",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TicketDependencyForm(forms.ModelForm):
|
class TicketDependencyForm(forms.ModelForm):
|
||||||
''' Adds a different ticket as a dependency for this Ticket '''
|
"""Adds a different ticket as a dependency for this Ticket"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TicketDependency
|
model = TicketDependency
|
||||||
fields = ('depends_on',)
|
fields = ("depends_on",)
|
||||||
|
|
||||||
def __init__(self, ticket, *args, **kwargs):
|
def __init__(self, ticket, *args, **kwargs):
|
||||||
super(TicketDependencyForm,self).__init__(*args, **kwargs)
|
super(TicketDependencyForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Only open tickets except myself, existing dependencies and parents
|
# Only open tickets except myself, existing dependencies and parents
|
||||||
self.fields['depends_on'].queryset = Ticket.objects.filter(status__in=Ticket.OPEN_STATUSES).exclude(id=ticket.id).exclude(depends_on__ticket=ticket).exclude(ticketdependency__depends_on=ticket)
|
self.fields["depends_on"].queryset = (
|
||||||
|
Ticket.objects.filter(status__in=Ticket.OPEN_STATUSES)
|
||||||
|
.exclude(id=ticket.id)
|
||||||
|
.exclude(depends_on__ticket=ticket)
|
||||||
|
.exclude(ticketdependency__depends_on=ticket)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TicketResolvesForm(forms.ModelForm):
|
class TicketResolvesForm(forms.ModelForm):
|
||||||
''' Adds this ticket as a dependency for a different ticket '''
|
"""Adds this ticket as a dependency for a different ticket"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TicketDependency
|
model = TicketDependency
|
||||||
fields = ('ticket',)
|
fields = ("ticket",)
|
||||||
|
|
||||||
def __init__(self, ticket, *args, **kwargs):
|
def __init__(self, ticket, *args, **kwargs):
|
||||||
super(TicketResolvesForm,self).__init__(*args, **kwargs)
|
super(TicketResolvesForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Only open tickets except myself, existing dependencies and parents
|
# Only open tickets except myself, existing dependencies and parents
|
||||||
self.fields['ticket'].queryset = Ticket.objects.exclude(status__in=Ticket.OPEN_STATUSES).exclude(id=ticket.id).exclude(depends_on__ticket=ticket).exclude(ticketdependency__depends_on=ticket)
|
self.fields["ticket"].queryset = (
|
||||||
|
Ticket.objects.exclude(status__in=Ticket.OPEN_STATUSES)
|
||||||
|
.exclude(id=ticket.id)
|
||||||
|
.exclude(depends_on__ticket=ticket)
|
||||||
|
.exclude(ticketdependency__depends_on=ticket)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MultipleTicketSelectForm(forms.Form):
|
class MultipleTicketSelectForm(forms.Form):
|
||||||
tickets = forms.ModelMultipleChoiceField(
|
tickets = forms.ModelMultipleChoiceField(
|
||||||
label=_('Tickets to merge'),
|
label=_("Tickets to merge"),
|
||||||
queryset=Ticket.objects.filter(merged_to=None),
|
queryset=Ticket.objects.filter(merged_to=None),
|
||||||
widget=forms.SelectMultiple(attrs={'class': 'form-control'})
|
widget=forms.SelectMultiple(attrs={"class": "form-control"}),
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean_tickets(self):
|
def clean_tickets(self):
|
||||||
tickets = self.cleaned_data.get('tickets')
|
tickets = self.cleaned_data.get("tickets")
|
||||||
if len(tickets) < 2:
|
if len(tickets) < 2:
|
||||||
raise ValidationError(_('Please choose at least 2 tickets.'))
|
raise ValidationError(_("Please choose at least 2 tickets."))
|
||||||
if len(tickets) > 4:
|
if len(tickets) > 4:
|
||||||
raise ValidationError(
|
raise ValidationError(_("Impossible to merge more than 4 tickets..."))
|
||||||
_('Impossible to merge more than 4 tickets...'))
|
queues = tickets.order_by("queue").distinct().values_list("queue", flat=True)
|
||||||
queues = tickets.order_by('queue').distinct(
|
|
||||||
).values_list('queue', flat=True)
|
|
||||||
if len(queues) != 1:
|
if len(queues) != 1:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('All selected tickets must share the same queue in order to be merged.'))
|
_(
|
||||||
|
"All selected tickets must share the same queue in order to be merged."
|
||||||
|
)
|
||||||
|
)
|
||||||
return tickets
|
return tickets
|
||||||
|
|
||||||
|
|
||||||
class ChecklistTemplateForm(forms.ModelForm):
|
class ChecklistTemplateForm(forms.ModelForm):
|
||||||
name = forms.CharField(
|
name = forms.CharField(
|
||||||
widget=forms.TextInput(attrs={'class': 'form-control'}),
|
widget=forms.TextInput(attrs={"class": "form-control"}),
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
task_list = forms.JSONField(widget=forms.HiddenInput())
|
task_list = forms.JSONField(widget=forms.HiddenInput())
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ChecklistTemplate
|
model = ChecklistTemplate
|
||||||
fields = ('name', 'task_list')
|
fields = ("name", "task_list")
|
||||||
|
|
||||||
def clean_task_list(self):
|
def clean_task_list(self):
|
||||||
task_list = self.cleaned_data['task_list']
|
task_list = self.cleaned_data["task_list"]
|
||||||
return list(map(lambda task: task.strip(), task_list))
|
return list(map(lambda task: task.strip(), task_list))
|
||||||
|
|
||||||
|
|
||||||
class ChecklistForm(forms.ModelForm):
|
class ChecklistForm(forms.ModelForm):
|
||||||
name = forms.CharField(
|
name = forms.CharField(
|
||||||
widget=forms.TextInput(attrs={'class': 'form-control'}),
|
widget=forms.TextInput(attrs={"class": "form-control"}),
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Checklist
|
model = Checklist
|
||||||
fields = ('name',)
|
fields = ("name",)
|
||||||
|
|
||||||
|
|
||||||
class CreateChecklistForm(ChecklistForm):
|
class CreateChecklistForm(ChecklistForm):
|
||||||
checklist_template = forms.ModelChoiceField(
|
checklist_template = forms.ModelChoiceField(
|
||||||
label=_("Template"),
|
label=_("Template"),
|
||||||
queryset=ChecklistTemplate.objects.all(),
|
queryset=ChecklistTemplate.objects.all(),
|
||||||
widget=forms.Select(attrs={'class': 'form-control'}),
|
widget=forms.Select(attrs={"class": "form-control"}),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(ChecklistForm.Meta):
|
class Meta(ChecklistForm.Meta):
|
||||||
fields = ('checklist_template', 'name')
|
fields = ("checklist_template", "name")
|
||||||
|
|
||||||
|
|
||||||
class FormControlDeleteFormSet(forms.BaseInlineFormSet):
|
class FormControlDeleteFormSet(forms.BaseInlineFormSet):
|
||||||
deletion_widget = forms.CheckboxInput(attrs={'class': 'form-control'})
|
deletion_widget = forms.CheckboxInput(attrs={"class": "form-control"})
|
||||||
|
145
helpdesk/lib.py
145
helpdesk/lib.py
@ -6,34 +6,52 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
|
|||||||
lib.py - Common functions (eg multipart e-mail)
|
lib.py - Common functions (eg multipart e-mail)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from datetime import date, datetime, time
|
from datetime import date, datetime, time
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError, ImproperlyConfigured
|
from django.core.exceptions import ValidationError, ImproperlyConfigured
|
||||||
from django.utils.encoding import smart_str
|
from django.utils.encoding import smart_str
|
||||||
from helpdesk.settings import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT
|
from helpdesk.settings import (
|
||||||
|
CUSTOMFIELD_DATE_FORMAT,
|
||||||
|
CUSTOMFIELD_DATETIME_FORMAT,
|
||||||
|
CUSTOMFIELD_TIME_FORMAT,
|
||||||
|
)
|
||||||
import logging
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('helpdesk')
|
logger = logging.getLogger("helpdesk")
|
||||||
|
|
||||||
|
|
||||||
def ticket_template_context(ticket):
|
def ticket_template_context(ticket):
|
||||||
context = {}
|
context = {}
|
||||||
|
|
||||||
for field in ('title', 'created', 'modified', 'submitter_email',
|
for field in (
|
||||||
'status', 'get_status_display', 'on_hold', 'description',
|
"title",
|
||||||
'resolution', 'priority', 'get_priority_display',
|
"created",
|
||||||
'last_escalation', 'ticket', 'ticket_for_url', 'merged_to',
|
"modified",
|
||||||
'get_status', 'ticket_url', 'staff_url', '_get_assigned_to'
|
"submitter_email",
|
||||||
):
|
"status",
|
||||||
|
"get_status_display",
|
||||||
|
"on_hold",
|
||||||
|
"description",
|
||||||
|
"resolution",
|
||||||
|
"priority",
|
||||||
|
"get_priority_display",
|
||||||
|
"last_escalation",
|
||||||
|
"ticket",
|
||||||
|
"ticket_for_url",
|
||||||
|
"merged_to",
|
||||||
|
"get_status",
|
||||||
|
"ticket_url",
|
||||||
|
"staff_url",
|
||||||
|
"_get_assigned_to",
|
||||||
|
):
|
||||||
attr = getattr(ticket, field, None)
|
attr = getattr(ticket, field, None)
|
||||||
if callable(attr):
|
if callable(attr):
|
||||||
context[field] = '%s' % attr()
|
context[field] = "%s" % attr()
|
||||||
else:
|
else:
|
||||||
context[field] = attr
|
context[field] = attr
|
||||||
context['assigned_to'] = context['_get_assigned_to']
|
context["assigned_to"] = context["_get_assigned_to"]
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -41,7 +59,7 @@ def ticket_template_context(ticket):
|
|||||||
def queue_template_context(queue):
|
def queue_template_context(queue):
|
||||||
context = {}
|
context = {}
|
||||||
|
|
||||||
for field in ('title', 'slug', 'email_address', 'from_address', 'locale'):
|
for field in ("title", "slug", "email_address", "from_address", "locale"):
|
||||||
attr = getattr(queue, field, None)
|
attr = getattr(queue, field, None)
|
||||||
if callable(attr):
|
if callable(attr):
|
||||||
context[field] = attr()
|
context[field] = attr()
|
||||||
@ -67,10 +85,10 @@ def safe_template_context(ticket):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'queue': queue_template_context(ticket.queue),
|
"queue": queue_template_context(ticket.queue),
|
||||||
'ticket': ticket_template_context(ticket),
|
"ticket": ticket_template_context(ticket),
|
||||||
}
|
}
|
||||||
context['ticket']['queue'] = context['queue']
|
context["ticket"]["queue"] = context["queue"]
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -87,41 +105,42 @@ def text_is_spam(text, request):
|
|||||||
return False
|
return False
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
try:
|
try:
|
||||||
site = Site.objects.get_current()
|
site = Site.objects.get_current()
|
||||||
except ImproperlyConfigured:
|
except ImproperlyConfigured:
|
||||||
site = Site(domain='configure-django-sites.com')
|
site = Site(domain="configure-django-sites.com")
|
||||||
|
|
||||||
# see https://akismet.readthedocs.io/en/latest/overview.html#using-akismet
|
# see https://akismet.readthedocs.io/en/latest/overview.html#using-akismet
|
||||||
|
|
||||||
apikey = None
|
apikey = None
|
||||||
|
|
||||||
if hasattr(settings, 'TYPEPAD_ANTISPAM_API_KEY'):
|
if hasattr(settings, "TYPEPAD_ANTISPAM_API_KEY"):
|
||||||
apikey = settings.TYPEPAD_ANTISPAM_API_KEY
|
apikey = settings.TYPEPAD_ANTISPAM_API_KEY
|
||||||
elif hasattr(settings, 'PYTHON_AKISMET_API_KEY'):
|
elif hasattr(settings, "PYTHON_AKISMET_API_KEY"):
|
||||||
# new env var expected by python-akismet package
|
# new env var expected by python-akismet package
|
||||||
apikey = settings.PYTHON_AKISMET_API_KEY
|
apikey = settings.PYTHON_AKISMET_API_KEY
|
||||||
elif hasattr(settings, 'AKISMET_API_KEY'):
|
elif hasattr(settings, "AKISMET_API_KEY"):
|
||||||
# deprecated, but kept for backward compatibility
|
# deprecated, but kept for backward compatibility
|
||||||
apikey = settings.AKISMET_API_KEY
|
apikey = settings.AKISMET_API_KEY
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
ak = Akismet(
|
ak = Akismet(
|
||||||
blog_url='http://%s/' % site.domain,
|
blog_url="http://%s/" % site.domain,
|
||||||
key=apikey,
|
key=apikey,
|
||||||
)
|
)
|
||||||
|
|
||||||
if hasattr(settings, 'TYPEPAD_ANTISPAM_API_KEY'):
|
if hasattr(settings, "TYPEPAD_ANTISPAM_API_KEY"):
|
||||||
ak.baseurl = 'api.antispam.typepad.com/1.1/'
|
ak.baseurl = "api.antispam.typepad.com/1.1/"
|
||||||
|
|
||||||
if ak.verify_key():
|
if ak.verify_key():
|
||||||
ak_data = {
|
ak_data = {
|
||||||
'user_ip': request.META.get('REMOTE_ADDR', '127.0.0.1'),
|
"user_ip": request.META.get("REMOTE_ADDR", "127.0.0.1"),
|
||||||
'user_agent': request.headers.get('User-Agent', ''),
|
"user_agent": request.headers.get("User-Agent", ""),
|
||||||
'referrer': request.headers.get('Referer', ''),
|
"referrer": request.headers.get("Referer", ""),
|
||||||
'comment_type': 'comment',
|
"comment_type": "comment",
|
||||||
'comment_author': '',
|
"comment_author": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
return ak.comment_check(smart_str(text), data=ak_data)
|
return ak.comment_check(smart_str(text), data=ak_data)
|
||||||
@ -131,12 +150,12 @@ def text_is_spam(text, request):
|
|||||||
|
|
||||||
def process_attachments(followup, attached_files):
|
def process_attachments(followup, attached_files):
|
||||||
max_email_attachment_size = getattr(
|
max_email_attachment_size = getattr(
|
||||||
settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000)
|
settings, "HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE", 512000
|
||||||
|
)
|
||||||
attachments = []
|
attachments = []
|
||||||
errors = set()
|
errors = set()
|
||||||
|
|
||||||
for attached in attached_files:
|
for attached in attached_files:
|
||||||
|
|
||||||
if attached.size:
|
if attached.size:
|
||||||
from helpdesk.models import FollowUpAttachment
|
from helpdesk.models import FollowUpAttachment
|
||||||
|
|
||||||
@ -145,9 +164,9 @@ def process_attachments(followup, attached_files):
|
|||||||
followup=followup,
|
followup=followup,
|
||||||
file=attached,
|
file=attached,
|
||||||
filename=filename,
|
filename=filename,
|
||||||
mime_type=attached.content_type or
|
mime_type=attached.content_type
|
||||||
mimetypes.guess_type(filename, strict=False)[0] or
|
or mimetypes.guess_type(filename, strict=False)[0]
|
||||||
'application/octet-stream',
|
or "application/octet-stream",
|
||||||
size=attached.size,
|
size=attached.size,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
@ -176,7 +195,7 @@ def format_time_spent(time_spent):
|
|||||||
if time_spent:
|
if time_spent:
|
||||||
time_spent = "{0:02d}h:{1:02d}m".format(
|
time_spent = "{0:02d}h:{1:02d}m".format(
|
||||||
int(time_spent.total_seconds()) // 3600,
|
int(time_spent.total_seconds()) // 3600,
|
||||||
int(time_spent.total_seconds()) % 3600 // 60
|
int(time_spent.total_seconds()) % 3600 // 60,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
time_spent = ""
|
time_spent = ""
|
||||||
@ -184,7 +203,7 @@ def format_time_spent(time_spent):
|
|||||||
|
|
||||||
|
|
||||||
def convert_value(value):
|
def convert_value(value):
|
||||||
""" Convert date/time data type to known fixed format string """
|
"""Convert date/time data type to known fixed format string"""
|
||||||
if type(value) is datetime:
|
if type(value) is datetime:
|
||||||
return value.strftime(CUSTOMFIELD_DATETIME_FORMAT)
|
return value.strftime(CUSTOMFIELD_DATETIME_FORMAT)
|
||||||
elif type(value) is date:
|
elif type(value) is date:
|
||||||
@ -201,39 +220,65 @@ def daily_time_spent_calculation(earliest, latest, open_hours):
|
|||||||
time_spent_seconds = 0
|
time_spent_seconds = 0
|
||||||
|
|
||||||
# avoid rendering day in different locale
|
# avoid rendering day in different locale
|
||||||
weekday = ('monday', 'tuesday', 'wednesday', 'thursday',
|
weekday = (
|
||||||
'friday', 'saturday', 'sunday')[earliest.weekday()]
|
"monday",
|
||||||
|
"tuesday",
|
||||||
|
"wednesday",
|
||||||
|
"thursday",
|
||||||
|
"friday",
|
||||||
|
"saturday",
|
||||||
|
"sunday",
|
||||||
|
)[earliest.weekday()]
|
||||||
|
|
||||||
# enforce correct settings
|
# enforce correct settings
|
||||||
MIDNIGHT = 23.9999
|
MIDNIGHT = 23.9999
|
||||||
start, end = open_hours.get(weekday, (0, MIDNIGHT))
|
start, end = open_hours.get(weekday, (0, MIDNIGHT))
|
||||||
if not 0 <= start <= end <= MIDNIGHT:
|
if not 0 <= start <= end <= MIDNIGHT:
|
||||||
raise ImproperlyConfigured("HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS"
|
raise ImproperlyConfigured(
|
||||||
f" setting for {weekday} out of (0, 23.9999) boundary")
|
"HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS"
|
||||||
|
f" setting for {weekday} out of (0, 23.9999) boundary"
|
||||||
|
)
|
||||||
|
|
||||||
# transform decimals to minutes and seconds
|
# transform decimals to minutes and seconds
|
||||||
start_hour, start_minute, start_second = int(start), int(start % 1 * 60), int(start * 60 % 1 * 60)
|
start_hour, start_minute, start_second = (
|
||||||
end_hour, end_minute, end_second = int(end), int(end % 1 * 60), int(end * 60 % 1 * 60)
|
int(start),
|
||||||
|
int(start % 1 * 60),
|
||||||
|
int(start * 60 % 1 * 60),
|
||||||
|
)
|
||||||
|
end_hour, end_minute, end_second = (
|
||||||
|
int(end),
|
||||||
|
int(end % 1 * 60),
|
||||||
|
int(end * 60 % 1 * 60),
|
||||||
|
)
|
||||||
|
|
||||||
# translate time for delta calculation
|
# translate time for delta calculation
|
||||||
earliest_f = earliest.hour + earliest.minute / 60 + earliest.second / 3600
|
earliest_f = earliest.hour + earliest.minute / 60 + earliest.second / 3600
|
||||||
latest_f = latest.hour + latest.minute / 60 + latest.second / (60 * 60) + latest.microsecond / (60 * 60 * 999999)
|
latest_f = (
|
||||||
|
latest.hour
|
||||||
|
+ latest.minute / 60
|
||||||
|
+ latest.second / (60 * 60)
|
||||||
|
+ latest.microsecond / (60 * 60 * 999999)
|
||||||
|
)
|
||||||
|
|
||||||
# if latest time is midnight and close hour is midnight, add a second to the time spent
|
# if latest time is midnight and close hour is midnight, add a second to the time spent
|
||||||
if latest_f >= MIDNIGHT and end == MIDNIGHT:
|
if latest_f >= MIDNIGHT and end == MIDNIGHT:
|
||||||
time_spent_seconds += 1
|
time_spent_seconds += 1
|
||||||
|
|
||||||
if earliest_f < start:
|
if earliest_f < start:
|
||||||
earliest = earliest.replace(hour=start_hour, minute=start_minute, second=start_second)
|
earliest = earliest.replace(
|
||||||
|
hour=start_hour, minute=start_minute, second=start_second
|
||||||
|
)
|
||||||
elif earliest_f >= end:
|
elif earliest_f >= end:
|
||||||
earliest = earliest.replace(hour=end_hour, minute=end_minute, second=end_second)
|
earliest = earliest.replace(hour=end_hour, minute=end_minute, second=end_second)
|
||||||
|
|
||||||
if latest_f < start:
|
if latest_f < start:
|
||||||
latest = latest.replace(hour=start_hour, minute=start_minute, second=start_second)
|
latest = latest.replace(
|
||||||
|
hour=start_hour, minute=start_minute, second=start_second
|
||||||
|
)
|
||||||
elif latest_f >= end:
|
elif latest_f >= end:
|
||||||
latest = latest.replace(hour=end_hour, minute=end_minute, second=end_second)
|
latest = latest.replace(hour=end_hour, minute=end_minute, second=end_second)
|
||||||
|
|
||||||
day_delta = latest - earliest
|
day_delta = latest - earliest
|
||||||
time_spent_seconds += day_delta.seconds
|
time_spent_seconds += day_delta.seconds
|
||||||
|
|
||||||
return time_spent_seconds
|
return time_spent_seconds
|
||||||
|
@ -14,56 +14,56 @@ from django.core.management.base import BaseCommand, CommandError
|
|||||||
from helpdesk.models import EscalationExclusion, Queue
|
from helpdesk.models import EscalationExclusion, Queue
|
||||||
|
|
||||||
day_names = {
|
day_names = {
|
||||||
'monday': 0,
|
"monday": 0,
|
||||||
'tuesday': 1,
|
"tuesday": 1,
|
||||||
'wednesday': 2,
|
"wednesday": 2,
|
||||||
'thursday': 3,
|
"thursday": 3,
|
||||||
'friday': 4,
|
"friday": 4,
|
||||||
'saturday': 5,
|
"saturday": 5,
|
||||||
'sunday': 6,
|
"sunday": 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-d',
|
"-d",
|
||||||
'--days',
|
"--days",
|
||||||
nargs='*',
|
nargs="*",
|
||||||
choices=list(day_names.keys()),
|
choices=list(day_names.keys()),
|
||||||
required=True,
|
required=True,
|
||||||
help='Days of week (monday, tuesday, etc). Enter the days as space separated list.'
|
help="Days of week (monday, tuesday, etc). Enter the days as space separated list.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-o',
|
"-o",
|
||||||
'--occurrences',
|
"--occurrences",
|
||||||
default=1,
|
default=1,
|
||||||
type=int,
|
type=int,
|
||||||
help='Occurrences: How many weeks ahead to exclude this day'
|
help="Occurrences: How many weeks ahead to exclude this day",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-q',
|
"-q",
|
||||||
'--queues',
|
"--queues",
|
||||||
nargs='*',
|
nargs="*",
|
||||||
choices=list(Queue.objects.values_list('slug', flat=True)),
|
choices=list(Queue.objects.values_list("slug", flat=True)),
|
||||||
help='Queues to include (default: all). Enter the queues slug as space separated list.'
|
help="Queues to include (default: all). Enter the queues slug as space separated list.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-x',
|
"-x",
|
||||||
'--exclude-verbosely',
|
"--exclude-verbosely",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help='Display a list of dates excluded'
|
help="Display a list of dates excluded",
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
days = options['days']
|
days = options["days"]
|
||||||
occurrences = options['occurrences']
|
occurrences = options["occurrences"]
|
||||||
verbose = options['exclude_verbosely']
|
verbose = options["exclude_verbosely"]
|
||||||
queue_slugs = options['queues']
|
queue_slugs = options["queues"]
|
||||||
|
|
||||||
if not (days and occurrences):
|
if not (days and occurrences):
|
||||||
raise CommandError('One or more occurrences must be specified.')
|
raise CommandError("One or more occurrences must be specified.")
|
||||||
|
|
||||||
queues = []
|
queues = []
|
||||||
if queue_slugs is not None:
|
if queue_slugs is not None:
|
||||||
@ -77,12 +77,13 @@ class Command(BaseCommand):
|
|||||||
if day == workdate.weekday():
|
if day == workdate.weekday():
|
||||||
if EscalationExclusion.objects.filter(date=workdate).count() == 0:
|
if EscalationExclusion.objects.filter(date=workdate).count() == 0:
|
||||||
esc = EscalationExclusion.objects.create(
|
esc = EscalationExclusion.objects.create(
|
||||||
name=f'Auto Exclusion for {day_name}',
|
name=f"Auto Exclusion for {day_name}", date=workdate
|
||||||
date=workdate
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
self.stdout.write(f"Created exclusion for {day_name} {workdate}")
|
self.stdout.write(
|
||||||
|
f"Created exclusion for {day_name} {workdate}"
|
||||||
|
)
|
||||||
|
|
||||||
for q in queues:
|
for q in queues:
|
||||||
esc.queues.add(q)
|
esc.queues.add(q)
|
||||||
|
@ -22,25 +22,24 @@ from helpdesk.models import Queue
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-q',
|
"-q",
|
||||||
'--queues',
|
"--queues",
|
||||||
nargs='*',
|
nargs="*",
|
||||||
choices=list(Queue.objects.values_list('slug', flat=True)),
|
choices=list(Queue.objects.values_list("slug", flat=True)),
|
||||||
help='Queues to include (default: all). Enter the queues slug as space separated list.'
|
help="Queues to include (default: all). Enter the queues slug as space separated list.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-x',
|
"-x",
|
||||||
'--escalate-verbosely',
|
"--escalate-verbosely",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help='Display a list of dates excluded'
|
help="Display a list of dates excluded",
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
queue_slugs = options['queues']
|
queue_slugs = options["queues"]
|
||||||
|
|
||||||
if queue_slugs is not None:
|
if queue_slugs is not None:
|
||||||
queues = Queue.objects.filter(slug__in=queue_slugs)
|
queues = Queue.objects.filter(slug__in=queue_slugs)
|
||||||
@ -53,16 +52,17 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
if q.permission_name:
|
if q.permission_name:
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
f" .. already has `permission_name={q.permission_name}`")
|
f" .. already has `permission_name={q.permission_name}`"
|
||||||
|
)
|
||||||
basename = q.permission_name[9:]
|
basename = q.permission_name[9:]
|
||||||
else:
|
else:
|
||||||
basename = q.generate_permission_name()
|
basename = q.generate_permission_name()
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
f" .. generated `permission_name={q.permission_name}`")
|
f" .. generated `permission_name={q.permission_name}`"
|
||||||
|
)
|
||||||
q.save()
|
q.save()
|
||||||
|
|
||||||
self.stdout.write(
|
self.stdout.write(f" .. checking permission codename `{basename}`")
|
||||||
f" .. checking permission codename `{basename}`")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
Permission.objects.create(
|
Permission.objects.create(
|
||||||
|
@ -20,10 +20,12 @@ User = get_user_model()
|
|||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""create_usersettings command"""
|
"""create_usersettings command"""
|
||||||
|
|
||||||
help = _('Check for user without django-helpdesk UserSettings '
|
help = _(
|
||||||
'and create settings if required. Uses '
|
"Check for user without django-helpdesk UserSettings "
|
||||||
'settings.DEFAULT_USER_SETTINGS which can be overridden to '
|
"and create settings if required. Uses "
|
||||||
'suit your situation.')
|
"settings.DEFAULT_USER_SETTINGS which can be overridden to "
|
||||||
|
"suit your situation."
|
||||||
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
"""handle command line"""
|
"""handle command line"""
|
||||||
|
@ -20,34 +20,36 @@ from helpdesk.models import EscalationExclusion, Queue, Ticket
|
|||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-q',
|
"-q",
|
||||||
'--queues',
|
"--queues",
|
||||||
nargs='*',
|
nargs="*",
|
||||||
choices=list(Queue.objects.values_list('slug', flat=True)),
|
choices=list(Queue.objects.values_list("slug", flat=True)),
|
||||||
help='Queues to include (default: all). Enter the queues slug as space separated list.'
|
help="Queues to include (default: all). Enter the queues slug as space separated list.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-x',
|
"-x",
|
||||||
'--escalate-verbosely',
|
"--escalate-verbosely",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help='Display escalated tickets'
|
help="Display escalated tickets",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-n',
|
"-n",
|
||||||
'--notify-only',
|
"--notify-only",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help='Send email reminder but dont escalate tickets'
|
help="Send email reminder but dont escalate tickets",
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
verbose = options['escalate_verbosely']
|
verbose = options["escalate_verbosely"]
|
||||||
notify_only = options['notify_only']
|
notify_only = options["notify_only"]
|
||||||
|
|
||||||
queue_slugs = options['queues']
|
queue_slugs = options["queues"]
|
||||||
# Only include queues with escalation configured
|
# Only include queues with escalation configured
|
||||||
queues = Queue.objects.filter(escalate_days__isnull=False).exclude(escalate_days=0)
|
queues = Queue.objects.filter(escalate_days__isnull=False).exclude(
|
||||||
|
escalate_days=0
|
||||||
|
)
|
||||||
if queue_slugs is not None:
|
if queue_slugs is not None:
|
||||||
queues = queues.filter(slug__in=queue_slugs)
|
queues = queues.filter(slug__in=queue_slugs)
|
||||||
|
|
||||||
@ -68,17 +70,15 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
req_last_escl_date = timezone.now() - timedelta(days=days)
|
req_last_escl_date = timezone.now() - timedelta(days=days)
|
||||||
|
|
||||||
for ticket in queue.ticket_set.filter(
|
for ticket in (
|
||||||
status__in=Ticket.OPEN_STATUSES
|
queue.ticket_set.filter(status__in=Ticket.OPEN_STATUSES)
|
||||||
).exclude(
|
.exclude(priority=1)
|
||||||
priority=1
|
.filter(Q(on_hold__isnull=True) | Q(on_hold=False))
|
||||||
).filter(
|
.filter(
|
||||||
Q(on_hold__isnull=True) | Q(on_hold=False)
|
Q(last_escalation__lte=req_last_escl_date)
|
||||||
).filter(
|
| Q(last_escalation__isnull=True, created__lte=req_last_escl_date)
|
||||||
Q(last_escalation__lte=req_last_escl_date) |
|
)
|
||||||
Q(last_escalation__isnull=True, created__lte=req_last_escl_date)
|
|
||||||
):
|
):
|
||||||
|
|
||||||
ticket.last_escalation = timezone.now()
|
ticket.last_escalation = timezone.now()
|
||||||
ticket.priority -= 1
|
ticket.priority -= 1
|
||||||
ticket.save()
|
ticket.save()
|
||||||
@ -86,24 +86,29 @@ class Command(BaseCommand):
|
|||||||
context = safe_template_context(ticket)
|
context = safe_template_context(ticket)
|
||||||
|
|
||||||
ticket.send(
|
ticket.send(
|
||||||
{'submitter': ('escalated_submitter', context),
|
{
|
||||||
'ticket_cc': ('escalated_cc', context),
|
"submitter": ("escalated_submitter", context),
|
||||||
'assigned_to': ('escalated_owner', context)},
|
"ticket_cc": ("escalated_cc", context),
|
||||||
|
"assigned_to": ("escalated_owner", context),
|
||||||
|
},
|
||||||
fail_silently=True,
|
fail_silently=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
self.stdout.write(f" - Esclating {ticket.ticket} from {ticket.priority + 1}>{ticket.priority}")
|
self.stdout.write(
|
||||||
|
f" - Esclating {ticket.ticket} from {ticket.priority + 1}>{ticket.priority}"
|
||||||
|
)
|
||||||
|
|
||||||
if not notify_only:
|
if not notify_only:
|
||||||
followup = ticket.followup_set.create(
|
followup = ticket.followup_set.create(
|
||||||
title=_('Ticket Escalated'),
|
title=_("Ticket Escalated"),
|
||||||
public=True,
|
public=True,
|
||||||
comment=_('Ticket escalated after %(nb)s days') % {'nb': queue.escalate_days},
|
comment=_("Ticket escalated after %(nb)s days")
|
||||||
|
% {"nb": queue.escalate_days},
|
||||||
)
|
)
|
||||||
|
|
||||||
followup.ticketchange_set.create(
|
followup.ticketchange_set.create(
|
||||||
field=_('Priority'),
|
field=_("Priority"),
|
||||||
old_value=ticket.priority + 1,
|
old_value=ticket.priority + 1,
|
||||||
new_value=ticket.priority,
|
new_value=ticket.priority,
|
||||||
)
|
)
|
||||||
|
@ -10,36 +10,38 @@ scripts/get_email.py - Designed to be run from cron, this script checks the
|
|||||||
helpdesk, creating tickets from the new messages (or
|
helpdesk, creating tickets from the new messages (or
|
||||||
adding to existing tickets if needed)
|
adding to existing tickets if needed)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from helpdesk.email import process_email
|
from helpdesk.email import process_email
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
help = (
|
||||||
help = 'Process django-helpdesk queues and process e-mails via POP3/IMAP or ' \
|
"Process django-helpdesk queues and process e-mails via POP3/IMAP or "
|
||||||
'from a local mailbox directory as required, feeding them into the helpdesk.'
|
"from a local mailbox directory as required, feeding them into the helpdesk."
|
||||||
|
)
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--quiet',
|
"--quiet",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
dest='quiet',
|
dest="quiet",
|
||||||
default=False,
|
default=False,
|
||||||
help='Hide details about each queue/message as they are processed',
|
help="Hide details about each queue/message as they are processed",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--debug_to_stdout',
|
"--debug_to_stdout",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
dest='debug_to_stdout',
|
dest="debug_to_stdout",
|
||||||
default=False,
|
default=False,
|
||||||
help='Log additional messaging to stdout.',
|
help="Log additional messaging to stdout.",
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
quiet = options.get('quiet')
|
quiet = options.get("quiet")
|
||||||
debug_to_stdout = options.get('debug_to_stdout')
|
debug_to_stdout = options.get("debug_to_stdout")
|
||||||
process_email(quiet=quiet, debug_to_stdout=debug_to_stdout)
|
process_email(quiet=quiet, debug_to_stdout=debug_to_stdout)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
process_email()
|
process_email()
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@ def pickle_settings(data):
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
import cPickle as pickle
|
import cPickle as pickle
|
||||||
from helpdesk.query import b64encode
|
from helpdesk.query import b64encode
|
||||||
|
|
||||||
return b64encode(pickle.dumps(data))
|
return b64encode(pickle.dumps(data))
|
||||||
|
|
||||||
|
|
||||||
@ -38,14 +39,12 @@ def populate_usersettings(apps, schema_editor):
|
|||||||
|
|
||||||
noop = lambda *args, **kwargs: None
|
noop = lambda *args, **kwargs: None
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0001_initial'),
|
("helpdesk", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RunPython(populate_usersettings, reverse_code=noop),
|
migrations.RunPython(populate_usersettings, reverse_code=noop),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,14 +4,15 @@ import os
|
|||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
|
|
||||||
fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures'))
|
fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../fixtures"))
|
||||||
fixture_filename = 'emailtemplate.json'
|
fixture_filename = "emailtemplate.json"
|
||||||
|
|
||||||
|
|
||||||
def deserialize_fixture():
|
def deserialize_fixture():
|
||||||
fixture_file = os.path.join(fixture_dir, fixture_filename)
|
fixture_file = os.path.join(fixture_dir, fixture_filename)
|
||||||
|
|
||||||
with open(fixture_file, 'rb') as fixture:
|
with open(fixture_file, "rb") as fixture:
|
||||||
return list(serializers.deserialize('json', fixture, ignorenonexistent=True))
|
return list(serializers.deserialize("json", fixture, ignorenonexistent=True))
|
||||||
|
|
||||||
|
|
||||||
def load_fixture(apps, schema_editor):
|
def load_fixture(apps, schema_editor):
|
||||||
@ -27,13 +28,12 @@ def unload_fixture(apps, schema_editor):
|
|||||||
objects = deserialize_fixture()
|
objects = deserialize_fixture()
|
||||||
|
|
||||||
EmailTemplate = apps.get_model("helpdesk", "emailtemplate")
|
EmailTemplate = apps.get_model("helpdesk", "emailtemplate")
|
||||||
EmailTemplate.objects.filter(pk__in=[ obj.object.pk for obj in objects ]).delete()
|
EmailTemplate.objects.filter(pk__in=[obj.object.pk for obj in objects]).delete()
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0002_populate_usersettings'),
|
("helpdesk", "0002_populate_usersettings"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -4,23 +4,42 @@ from django.conf import settings
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
('helpdesk', '0003_initial_data_import'),
|
("helpdesk", "0003_initial_data_import"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='QueueMembership',
|
name="QueueMembership",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
(
|
||||||
('queues', models.ManyToManyField(to='helpdesk.Queue', verbose_name='Authorized Queues')),
|
"id",
|
||||||
('user', models.OneToOneField(verbose_name='User', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
models.AutoField(
|
||||||
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"queues",
|
||||||
|
models.ManyToManyField(
|
||||||
|
to="helpdesk.Queue", verbose_name="Authorized Queues"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.OneToOneField(
|
||||||
|
verbose_name="User",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Queue Membership',
|
"verbose_name": "Queue Membership",
|
||||||
'verbose_name_plural': 'Queue Memberships',
|
"verbose_name_plural": "Queue Memberships",
|
||||||
},
|
},
|
||||||
bases=(models.Model,),
|
bases=(models.Model,),
|
||||||
),
|
),
|
||||||
|
@ -3,25 +3,36 @@ from django.db import models, migrations
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0004_add_per_queue_staff_membership'),
|
("helpdesk", "0004_add_per_queue_staff_membership"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='escalationexclusion',
|
model_name="escalationexclusion",
|
||||||
name='queues',
|
name="queues",
|
||||||
field=models.ManyToManyField(help_text='Leave blank for this exclusion to be applied to all queues, or select those queues you wish to exclude with this entry.', to='helpdesk.Queue', blank=True),
|
field=models.ManyToManyField(
|
||||||
|
help_text="Leave blank for this exclusion to be applied to all queues, or select those queues you wish to exclude with this entry.",
|
||||||
|
to="helpdesk.Queue",
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='ignoreemail',
|
model_name="ignoreemail",
|
||||||
name='queues',
|
name="queues",
|
||||||
field=models.ManyToManyField(help_text='Leave blank for this e-mail to be ignored on all queues, or select those queues you wish to ignore this e-mail for.', to='helpdesk.Queue', blank=True),
|
field=models.ManyToManyField(
|
||||||
|
help_text="Leave blank for this e-mail to be ignored on all queues, or select those queues you wish to ignore this e-mail for.",
|
||||||
|
to="helpdesk.Queue",
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='presetreply',
|
model_name="presetreply",
|
||||||
name='queues',
|
name="queues",
|
||||||
field=models.ManyToManyField(help_text='Leave blank to allow this reply to be used for all queues, or select those queues you wish to limit this reply to.', to='helpdesk.Queue', blank=True),
|
field=models.ManyToManyField(
|
||||||
|
help_text="Leave blank to allow this reply to be used for all queues, or select those queues you wish to limit this reply to.",
|
||||||
|
to="helpdesk.Queue",
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -3,25 +3,42 @@ from django.db import models, migrations
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0005_queues_no_null'),
|
("helpdesk", "0005_queues_no_null"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='email_address',
|
name="email_address",
|
||||||
field=models.EmailField(help_text='All outgoing e-mails for this queue will use this e-mail address. If you use IMAP or POP3, this should be the e-mail address for that mailbox.', max_length=254, null=True, verbose_name='E-Mail Address', blank=True),
|
field=models.EmailField(
|
||||||
|
help_text="All outgoing e-mails for this queue will use this e-mail address. If you use IMAP or POP3, this should be the e-mail address for that mailbox.",
|
||||||
|
max_length=254,
|
||||||
|
null=True,
|
||||||
|
verbose_name="E-Mail Address",
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='ticket',
|
model_name="ticket",
|
||||||
name='submitter_email',
|
name="submitter_email",
|
||||||
field=models.EmailField(help_text='The submitter will receive an email for all public follow-ups left for this task.', max_length=254, null=True, verbose_name='Submitter E-Mail', blank=True),
|
field=models.EmailField(
|
||||||
|
help_text="The submitter will receive an email for all public follow-ups left for this task.",
|
||||||
|
max_length=254,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Submitter E-Mail",
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='ticketcc',
|
model_name="ticketcc",
|
||||||
name='email',
|
name="email",
|
||||||
field=models.EmailField(help_text='For non-user followers, enter their e-mail address', max_length=254, null=True, verbose_name='E-Mail Address', blank=True),
|
field=models.EmailField(
|
||||||
|
help_text="For non-user followers, enter their e-mail address",
|
||||||
|
max_length=254,
|
||||||
|
null=True,
|
||||||
|
verbose_name="E-Mail Address",
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -3,15 +3,18 @@ from django.db import models, migrations
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0006_email_maxlength'),
|
("helpdesk", "0006_email_maxlength"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='customfield',
|
model_name="customfield",
|
||||||
name='label',
|
name="label",
|
||||||
field=models.CharField(help_text='The display label for this field', max_length=30, verbose_name='Label'),
|
field=models.CharField(
|
||||||
|
help_text="The display label for this field",
|
||||||
|
max_length=30,
|
||||||
|
verbose_name="Label",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -3,15 +3,20 @@ from django.db import models, migrations
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0007_max_length_by_integer'),
|
("helpdesk", "0007_max_length_by_integer"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='permission_name',
|
name="permission_name",
|
||||||
field=models.CharField(help_text='Name used in the django.contrib.auth permission system', max_length=50, null=True, verbose_name='Django auth permission name', blank=True),
|
field=models.CharField(
|
||||||
|
help_text="Name used in the django.contrib.auth permission system",
|
||||||
|
max_length=50,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Django auth permission name",
|
||||||
|
blank=True,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -7,14 +7,14 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
def create_and_assign_permissions(apps, schema_editor):
|
def create_and_assign_permissions(apps, schema_editor):
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
Permission = apps.get_model('auth', 'Permission')
|
Permission = apps.get_model("auth", "Permission")
|
||||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||||
# Two steps:
|
# Two steps:
|
||||||
# 1. Create the permission for existing Queues
|
# 1. Create the permission for existing Queues
|
||||||
# 2. Assign the permission to user according to QueueMembership objects
|
# 2. Assign the permission to user according to QueueMembership objects
|
||||||
|
|
||||||
# First step: prepare the permission for each queue
|
# First step: prepare the permission for each queue
|
||||||
Queue = apps.get_model('helpdesk', 'Queue')
|
Queue = apps.get_model("helpdesk", "Queue")
|
||||||
for q in Queue.objects.using(db_alias).all():
|
for q in Queue.objects.using(db_alias).all():
|
||||||
if not q.permission_name:
|
if not q.permission_name:
|
||||||
basename = "queue_access_%s" % q.slug
|
basename = "queue_access_%s" % q.slug
|
||||||
@ -35,7 +35,7 @@ def create_and_assign_permissions(apps, schema_editor):
|
|||||||
q.save()
|
q.save()
|
||||||
|
|
||||||
# Second step: map the permissions according to QueueMembership
|
# Second step: map the permissions according to QueueMembership
|
||||||
QueueMembership = apps.get_model('helpdesk', 'QueueMembership')
|
QueueMembership = apps.get_model("helpdesk", "QueueMembership")
|
||||||
for qm in QueueMembership.objects.using(db_alias).all():
|
for qm in QueueMembership.objects.using(db_alias).all():
|
||||||
user = qm.user
|
user = qm.user
|
||||||
for q in qm.queues.all():
|
for q in qm.queues.all():
|
||||||
@ -47,9 +47,9 @@ def create_and_assign_permissions(apps, schema_editor):
|
|||||||
|
|
||||||
def revert_queue_membership(apps, schema_editor):
|
def revert_queue_membership(apps, schema_editor):
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
Permission = apps.get_model('auth', 'Permission')
|
Permission = apps.get_model("auth", "Permission")
|
||||||
Queue = apps.get_model('helpdesk', 'Queue')
|
Queue = apps.get_model("helpdesk", "Queue")
|
||||||
QueueMembership = apps.get_model('helpdesk', 'QueueMembership')
|
QueueMembership = apps.get_model("helpdesk", "QueueMembership")
|
||||||
for p in Permission.objects.using(db_alias).all():
|
for p in Permission.objects.using(db_alias).all():
|
||||||
if p.codename.startswith("queue_access_"):
|
if p.codename.startswith("queue_access_"):
|
||||||
slug = p.codename[13:]
|
slug = p.codename[13:]
|
||||||
@ -66,12 +66,10 @@ def revert_queue_membership(apps, schema_editor):
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0008_extra_for_permissions'),
|
("helpdesk", "0008_extra_for_permissions"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RunPython(create_and_assign_permissions,
|
migrations.RunPython(create_and_assign_permissions, revert_queue_membership)
|
||||||
revert_queue_membership)
|
|
||||||
]
|
]
|
||||||
|
@ -3,21 +3,20 @@ from django.db import migrations
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0009_migrate_queuemembership'),
|
("helpdesk", "0009_migrate_queuemembership"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='queuemembership',
|
model_name="queuemembership",
|
||||||
name='queues',
|
name="queues",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='queuemembership',
|
model_name="queuemembership",
|
||||||
name='user',
|
name="user",
|
||||||
),
|
),
|
||||||
migrations.DeleteModel(
|
migrations.DeleteModel(
|
||||||
name='QueueMembership',
|
name="QueueMembership",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -3,20 +3,30 @@ from django.db import models, migrations
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0010_remove_queuemembership'),
|
("helpdesk", "0010_remove_queuemembership"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='permission_name',
|
name="permission_name",
|
||||||
field=models.CharField(editable=False, max_length=50, blank=True, help_text='Name used in the django.contrib.auth permission system', null=True, verbose_name='Django auth permission name'),
|
field=models.CharField(
|
||||||
|
editable=False,
|
||||||
|
max_length=50,
|
||||||
|
blank=True,
|
||||||
|
help_text="Name used in the django.contrib.auth permission system",
|
||||||
|
null=True,
|
||||||
|
verbose_name="Django auth permission name",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='slug',
|
name="slug",
|
||||||
field=models.SlugField(help_text="This slug is used when building ticket ID's. Once set, try not to change it or e-mailing may get messy.", unique=True, verbose_name='Slug'),
|
field=models.SlugField(
|
||||||
|
help_text="This slug is used when building ticket ID's. Once set, try not to change it or e-mailing may get messy.",
|
||||||
|
unique=True,
|
||||||
|
verbose_name="Slug",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -6,16 +6,22 @@ import django.db.models.deletion
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
('helpdesk', '0011_admin_related_improvements'),
|
("helpdesk", "0011_admin_related_improvements"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='default_owner',
|
name="default_owner",
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='default_owner', to=settings.AUTH_USER_MODEL, verbose_name='Default owner'),
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="default_owner",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="Default owner",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -4,30 +4,66 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0012_queue_default_owner'),
|
("helpdesk", "0012_queue_default_owner"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='email_box_local_dir',
|
name="email_box_local_dir",
|
||||||
field=models.CharField(blank=True, help_text='If using a local directory, what directory path do you wish to poll for new email? Example: /var/lib/mail/helpdesk/', max_length=200, null=True, verbose_name='E-Mail Local Directory'),
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text="If using a local directory, what directory path do you wish to poll for new email? Example: /var/lib/mail/helpdesk/",
|
||||||
|
max_length=200,
|
||||||
|
null=True,
|
||||||
|
verbose_name="E-Mail Local Directory",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='logging_dir',
|
name="logging_dir",
|
||||||
field=models.CharField(blank=True, help_text='If logging is enabled, what directory should we use to store log files for this queue? The standard logging mechanims are used if no directory is set', max_length=200, null=True, verbose_name='Logging Directory'),
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text="If logging is enabled, what directory should we use to store log files for this queue? The standard logging mechanims are used if no directory is set",
|
||||||
|
max_length=200,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Logging Directory",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='logging_type',
|
name="logging_type",
|
||||||
field=models.CharField(blank=True, choices=[('none', 'None'), ('debug', 'Debug'), ('info', 'Information'), ('warn', 'Warning'), ('error', 'Error'), ('crit', 'Critical')], help_text='Set the default logging level. All messages at that level or above will be logged to the directory set below. If no level is set, logging will be disabled.', max_length=5, null=True, verbose_name='Logging Type'),
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("none", "None"),
|
||||||
|
("debug", "Debug"),
|
||||||
|
("info", "Information"),
|
||||||
|
("warn", "Warning"),
|
||||||
|
("error", "Error"),
|
||||||
|
("crit", "Critical"),
|
||||||
|
],
|
||||||
|
help_text="Set the default logging level. All messages at that level or above will be logged to the directory set below. If no level is set, logging will be disabled.",
|
||||||
|
max_length=5,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Logging Type",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='email_box_type',
|
name="email_box_type",
|
||||||
field=models.CharField(blank=True, choices=[('pop3', 'POP 3'), ('imap', 'IMAP'), ('local', 'Local Directory')], help_text='E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.', max_length=5, null=True, verbose_name='E-Mail Box Type'),
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("pop3", "POP 3"),
|
||||||
|
("imap", "IMAP"),
|
||||||
|
("local", "Local Directory"),
|
||||||
|
],
|
||||||
|
help_text="E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.",
|
||||||
|
max_length=5,
|
||||||
|
null=True,
|
||||||
|
verbose_name="E-Mail Box Type",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -4,17 +4,18 @@ from django.conf import settings
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0013_email_box_local_dir_and_logging'),
|
("helpdesk", "0013_email_box_local_dir_and_logging"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='usersettings',
|
model_name="usersettings",
|
||||||
name='user',
|
name="user",
|
||||||
field=models.OneToOneField(to=settings.AUTH_USER_MODEL,
|
field=models.OneToOneField(
|
||||||
related_name='usersettings_helpdesk',
|
to=settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.CASCADE),
|
related_name="usersettings_helpdesk",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -4,15 +4,21 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0014_usersettings_related_name'),
|
("helpdesk", "0014_usersettings_related_name"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='permission_name',
|
name="permission_name",
|
||||||
field=models.CharField(blank=True, editable=False, help_text='Name used in the django.contrib.auth permission system', max_length=72, null=True, verbose_name='Django auth permission name'),
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
editable=False,
|
||||||
|
help_text="Name used in the django.contrib.auth permission system",
|
||||||
|
max_length=72,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Django auth permission name",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -4,38 +4,61 @@ from django.db import migrations
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0015_expand_permission_name_size'),
|
("helpdesk", "0015_expand_permission_name_size"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='attachment',
|
name="attachment",
|
||||||
options={'ordering': ('filename',), 'verbose_name': 'Attachment', 'verbose_name_plural': 'Attachments'},
|
options={
|
||||||
|
"ordering": ("filename",),
|
||||||
|
"verbose_name": "Attachment",
|
||||||
|
"verbose_name_plural": "Attachments",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='emailtemplate',
|
name="emailtemplate",
|
||||||
options={'ordering': ('template_name', 'locale'), 'verbose_name': 'e-mail template', 'verbose_name_plural': 'e-mail templates'},
|
options={
|
||||||
|
"ordering": ("template_name", "locale"),
|
||||||
|
"verbose_name": "e-mail template",
|
||||||
|
"verbose_name_plural": "e-mail templates",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='followup',
|
name="followup",
|
||||||
options={'ordering': ('date',), 'verbose_name': 'Follow-up', 'verbose_name_plural': 'Follow-ups'},
|
options={
|
||||||
|
"ordering": ("date",),
|
||||||
|
"verbose_name": "Follow-up",
|
||||||
|
"verbose_name_plural": "Follow-ups",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='kbcategory',
|
name="kbcategory",
|
||||||
options={'ordering': ('title',), 'verbose_name': 'Knowledge base category', 'verbose_name_plural': 'Knowledge base categories'},
|
options={
|
||||||
|
"ordering": ("title",),
|
||||||
|
"verbose_name": "Knowledge base category",
|
||||||
|
"verbose_name_plural": "Knowledge base categories",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='kbitem',
|
name="kbitem",
|
||||||
options={'ordering': ('title',), 'verbose_name': 'Knowledge base item', 'verbose_name_plural': 'Knowledge base items'},
|
options={
|
||||||
|
"ordering": ("title",),
|
||||||
|
"verbose_name": "Knowledge base item",
|
||||||
|
"verbose_name_plural": "Knowledge base items",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='presetreply',
|
name="presetreply",
|
||||||
options={'ordering': ('name',), 'verbose_name': 'Pre-set reply', 'verbose_name_plural': 'Pre-set replies'},
|
options={
|
||||||
|
"ordering": ("name",),
|
||||||
|
"verbose_name": "Pre-set reply",
|
||||||
|
"verbose_name_plural": "Pre-set replies",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='ticketcustomfieldvalue',
|
name="ticketcustomfieldvalue",
|
||||||
unique_together=set([('ticket', 'field')]),
|
unique_together=set([("ticket", "field")]),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -6,15 +6,21 @@ import django.db.models.deletion
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0016_alter_model_options'),
|
("helpdesk", "0016_alter_model_options"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='default_owner',
|
name="default_owner",
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_owner', to=settings.AUTH_USER_MODEL, verbose_name='Default owner'),
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="default_owner",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="Default owner",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -4,55 +4,99 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0017_default_owner_on_delete_null'),
|
("helpdesk", "0017_default_owner_on_delete_null"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='followup',
|
model_name="followup",
|
||||||
name='public',
|
name="public",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.', verbose_name='Public'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.",
|
||||||
|
verbose_name="Public",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='ignoreemail',
|
model_name="ignoreemail",
|
||||||
name='keep_in_mailbox',
|
name="keep_in_mailbox",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.', verbose_name='Save Emails in Mailbox?'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.",
|
||||||
|
verbose_name="Save Emails in Mailbox?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='allow_email_submission',
|
name="allow_email_submission",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='Do you want to poll the e-mail box below for new tickets?', verbose_name='Allow E-Mail Submission?'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Do you want to poll the e-mail box below for new tickets?",
|
||||||
|
verbose_name="Allow E-Mail Submission?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='allow_public_submission',
|
name="allow_public_submission",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='Should this queue be listed on the public submission form?', verbose_name='Allow Public Submission?'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Should this queue be listed on the public submission form?",
|
||||||
|
verbose_name="Allow Public Submission?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='email_box_ssl',
|
name="email_box_ssl",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.', verbose_name='Use SSL for E-Mail?'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.",
|
||||||
|
verbose_name="Use SSL for E-Mail?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='savedsearch',
|
model_name="savedsearch",
|
||||||
name='shared',
|
name="shared",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='Should other users see this query?', verbose_name='Shared With Other Users?'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Should other users see this query?",
|
||||||
|
verbose_name="Shared With Other Users?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='ticket',
|
model_name="ticket",
|
||||||
name='on_hold',
|
name="on_hold",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='If a ticket is on hold, it will not automatically be escalated.', verbose_name='On Hold'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="If a ticket is on hold, it will not automatically be escalated.",
|
||||||
|
verbose_name="On Hold",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='ticketcc',
|
model_name="ticketcc",
|
||||||
name='can_update',
|
name="can_update",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='Can this CC login and update the ticket?', verbose_name='Can Update Ticket?'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Can this CC login and update the ticket?",
|
||||||
|
verbose_name="Can Update Ticket?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='ticketcc',
|
model_name="ticketcc",
|
||||||
name='can_view',
|
name="can_view",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='Can this CC login to view the ticket details?', verbose_name='Can View Ticket?'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Can this CC login to view the ticket details?",
|
||||||
|
verbose_name="Can View Ticket?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -8,22 +8,24 @@ def clear_secret_keys(apps, schema_editor):
|
|||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
for ticket in Ticket.objects.using(db_alias).all():
|
for ticket in Ticket.objects.using(db_alias).all():
|
||||||
ticket.secret_key = ''
|
ticket.secret_key = ""
|
||||||
ticket.save()
|
ticket.save()
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0018_ticket_secret_key'),
|
("helpdesk", "0018_ticket_secret_key"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='ticket',
|
model_name="ticket",
|
||||||
name='secret_key',
|
name="secret_key",
|
||||||
field=models.CharField(default=helpdesk.models.mk_secret, max_length=36,
|
field=models.CharField(
|
||||||
verbose_name='Secret key needed for viewing/editing ticket by non-logged in users'),
|
default=helpdesk.models.mk_secret,
|
||||||
|
max_length=36,
|
||||||
|
verbose_name="Secret key needed for viewing/editing ticket by non-logged in users",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.RunPython(clear_secret_keys),
|
migrations.RunPython(clear_secret_keys),
|
||||||
]
|
]
|
||||||
|
@ -16,7 +16,7 @@ def unpickle_settings(settings_pickled):
|
|||||||
# Python 3 support
|
# Python 3 support
|
||||||
from base64 import decodebytes as b64decode
|
from base64 import decodebytes as b64decode
|
||||||
try:
|
try:
|
||||||
return pickle.loads(b64decode(settings_pickled.encode('utf-8')))
|
return pickle.loads(b64decode(settings_pickled.encode("utf-8")))
|
||||||
except Exception:
|
except Exception:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@ -33,41 +33,66 @@ def move_old_values(apps, schema_editor):
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0019_ticket_secret_key'),
|
("helpdesk", "0019_ticket_secret_key"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='usersettings',
|
model_name="usersettings",
|
||||||
name='email_on_ticket_assign',
|
name="email_on_ticket_assign",
|
||||||
field=models.BooleanField(default=helpdesk.models.email_on_ticket_assign_default, help_text='If you are assigned a ticket via the web, do you want to receive an e-mail?', verbose_name='E-mail me when assigned a ticket?'),
|
field=models.BooleanField(
|
||||||
|
default=helpdesk.models.email_on_ticket_assign_default,
|
||||||
|
help_text="If you are assigned a ticket via the web, do you want to receive an e-mail?",
|
||||||
|
verbose_name="E-mail me when assigned a ticket?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='usersettings',
|
model_name="usersettings",
|
||||||
name='email_on_ticket_change',
|
name="email_on_ticket_change",
|
||||||
field=models.BooleanField(default=helpdesk.models.email_on_ticket_change_default, help_text="If you're the ticket owner and the ticket is changed via the web by somebody else, do you want to receive an e-mail?", verbose_name='E-mail me on ticket change?'),
|
field=models.BooleanField(
|
||||||
|
default=helpdesk.models.email_on_ticket_change_default,
|
||||||
|
help_text="If you're the ticket owner and the ticket is changed via the web by somebody else, do you want to receive an e-mail?",
|
||||||
|
verbose_name="E-mail me on ticket change?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='usersettings',
|
model_name="usersettings",
|
||||||
name='login_view_ticketlist',
|
name="login_view_ticketlist",
|
||||||
field=models.BooleanField(default=helpdesk.models.login_view_ticketlist_default, help_text='Display the ticket list upon login? Otherwise, the dashboard is shown.', verbose_name='Show Ticket List on Login?'),
|
field=models.BooleanField(
|
||||||
|
default=helpdesk.models.login_view_ticketlist_default,
|
||||||
|
help_text="Display the ticket list upon login? Otherwise, the dashboard is shown.",
|
||||||
|
verbose_name="Show Ticket List on Login?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='usersettings',
|
model_name="usersettings",
|
||||||
name='tickets_per_page',
|
name="tickets_per_page",
|
||||||
field=models.IntegerField(choices=[(10, '10'), (25, '25'), (50, '50'), (100, '100')], default=helpdesk.models.tickets_per_page_default, help_text='How many tickets do you want to see on the Ticket List page?', verbose_name='Number of tickets to show per page'),
|
field=models.IntegerField(
|
||||||
|
choices=[(10, "10"), (25, "25"), (50, "50"), (100, "100")],
|
||||||
|
default=helpdesk.models.tickets_per_page_default,
|
||||||
|
help_text="How many tickets do you want to see on the Ticket List page?",
|
||||||
|
verbose_name="Number of tickets to show per page",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='usersettings',
|
model_name="usersettings",
|
||||||
name='use_email_as_submitter',
|
name="use_email_as_submitter",
|
||||||
field=models.BooleanField(default=helpdesk.models.use_email_as_submitter_default, help_text='When you submit a ticket, do you want to automatically use your e-mail address as the submitter address? You can type a different e-mail address when entering the ticket if needed, this option only changes the default.', verbose_name='Use my e-mail address when submitting tickets?'),
|
field=models.BooleanField(
|
||||||
|
default=helpdesk.models.use_email_as_submitter_default,
|
||||||
|
help_text="When you submit a ticket, do you want to automatically use your e-mail address as the submitter address? You can type a different e-mail address when entering the ticket if needed, this option only changes the default.",
|
||||||
|
verbose_name="Use my e-mail address when submitting tickets?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='usersettings',
|
model_name="usersettings",
|
||||||
name='settings_pickled',
|
name="settings_pickled",
|
||||||
field=models.TextField(blank=True, help_text='DEPRECATED! This is a base64-encoded representation of a pickled Python dictionary. Do not change this field via the admin.', null=True, verbose_name='DEPRECATED! Settings Dictionary DEPRECATED!'),
|
field=models.TextField(
|
||||||
|
blank=True,
|
||||||
|
help_text="DEPRECATED! This is a base64-encoded representation of a pickled Python dictionary. Do not change this field via the admin.",
|
||||||
|
null=True,
|
||||||
|
verbose_name="DEPRECATED! Settings Dictionary DEPRECATED!",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.RunPython(move_old_values),
|
migrations.RunPython(move_old_values),
|
||||||
]
|
]
|
||||||
|
@ -4,61 +4,105 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
('helpdesk', '0020_depickle_user_settings'),
|
("helpdesk", "0020_depickle_user_settings"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='kbitem',
|
model_name="kbitem",
|
||||||
name='voted_by',
|
name="voted_by",
|
||||||
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
|
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='followup',
|
model_name="followup",
|
||||||
name='public',
|
name="public",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.', verbose_name='Public'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.",
|
||||||
|
verbose_name="Public",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='ignoreemail',
|
model_name="ignoreemail",
|
||||||
name='keep_in_mailbox',
|
name="keep_in_mailbox",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.', verbose_name='Save Emails in Mailbox?'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.",
|
||||||
|
verbose_name="Save Emails in Mailbox?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='allow_email_submission',
|
name="allow_email_submission",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='Do you want to poll the e-mail box below for new tickets?', verbose_name='Allow E-Mail Submission?'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Do you want to poll the e-mail box below for new tickets?",
|
||||||
|
verbose_name="Allow E-Mail Submission?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='allow_public_submission',
|
name="allow_public_submission",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='Should this queue be listed on the public submission form?', verbose_name='Allow Public Submission?'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Should this queue be listed on the public submission form?",
|
||||||
|
verbose_name="Allow Public Submission?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='email_box_ssl',
|
name="email_box_ssl",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.', verbose_name='Use SSL for E-Mail?'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.",
|
||||||
|
verbose_name="Use SSL for E-Mail?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='savedsearch',
|
model_name="savedsearch",
|
||||||
name='shared',
|
name="shared",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='Should other users see this query?', verbose_name='Shared With Other Users?'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Should other users see this query?",
|
||||||
|
verbose_name="Shared With Other Users?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='ticket',
|
model_name="ticket",
|
||||||
name='on_hold',
|
name="on_hold",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='If a ticket is on hold, it will not automatically be escalated.', verbose_name='On Hold'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="If a ticket is on hold, it will not automatically be escalated.",
|
||||||
|
verbose_name="On Hold",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='ticketcc',
|
model_name="ticketcc",
|
||||||
name='can_update',
|
name="can_update",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='Can this CC login and update the ticket?', verbose_name='Can Update Ticket?'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Can this CC login and update the ticket?",
|
||||||
|
verbose_name="Can Update Ticket?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='ticketcc',
|
model_name="ticketcc",
|
||||||
name='can_view',
|
name="can_view",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='Can this CC login to view the ticket details?', verbose_name='Can View Ticket?'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Can this CC login to view the ticket details?",
|
||||||
|
verbose_name="Can View Ticket?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -4,15 +4,21 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0021_voting_tracker'),
|
("helpdesk", "0021_voting_tracker"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='followup',
|
model_name="followup",
|
||||||
name='message_id',
|
name="message_id",
|
||||||
field=models.CharField(blank=True, editable=False, help_text="The Message ID of the submitter's email.", max_length=256, null=True, verbose_name='E-Mail ID'),
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
editable=False,
|
||||||
|
help_text="The Message ID of the submitter's email.",
|
||||||
|
max_length=256,
|
||||||
|
null=True,
|
||||||
|
verbose_name="E-Mail ID",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -4,15 +4,18 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0022_add_submitter_email_id_field_to_ticket'),
|
("helpdesk", "0022_add_submitter_email_id_field_to_ticket"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='enable_notifications_on_email_events',
|
name="enable_notifications_on_email_events",
|
||||||
field=models.BooleanField(default=False, help_text='When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature', verbose_name='Notify contacts when email updates arrive'),
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature",
|
||||||
|
verbose_name="Notify contacts when email updates arrive",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -4,15 +4,16 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0023_add_enable_notifications_on_email_events_to_ticket'),
|
("helpdesk", "0023_add_enable_notifications_on_email_events_to_ticket"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='followup',
|
model_name="followup",
|
||||||
name='time_spent',
|
name="time_spent",
|
||||||
field=models.DurationField(blank=True, help_text='Time spent on this follow up', null=True),
|
field=models.DurationField(
|
||||||
|
blank=True, help_text="Time spent on this follow up", null=True
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -4,15 +4,18 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0024_time_spent'),
|
("helpdesk", "0024_time_spent"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='dedicated_time',
|
name="dedicated_time",
|
||||||
field=models.DurationField(blank=True, help_text='Time to be spent on this Queue in total', null=True),
|
field=models.DurationField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Time to be spent on this Queue in total",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -6,31 +6,63 @@ import helpdesk.models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0025_queue_dedicated_time'),
|
("helpdesk", "0025_queue_dedicated_time"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='KBIAttachment',
|
name="KBIAttachment",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('file', models.FileField(max_length=1000, upload_to=helpdesk.models.attachment_path, verbose_name='File')),
|
"id",
|
||||||
('filename', models.CharField(max_length=1000, verbose_name='Filename')),
|
models.AutoField(
|
||||||
('mime_type', models.CharField(max_length=255, verbose_name='MIME Type')),
|
auto_created=True,
|
||||||
('size', models.IntegerField(help_text='Size of this file in bytes', verbose_name='Size')),
|
primary_key=True,
|
||||||
('kbitem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='helpdesk.KBItem', verbose_name='Knowledge base item')),
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"file",
|
||||||
|
models.FileField(
|
||||||
|
max_length=1000,
|
||||||
|
upload_to=helpdesk.models.attachment_path,
|
||||||
|
verbose_name="File",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"filename",
|
||||||
|
models.CharField(max_length=1000, verbose_name="Filename"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"mime_type",
|
||||||
|
models.CharField(max_length=255, verbose_name="MIME Type"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"size",
|
||||||
|
models.IntegerField(
|
||||||
|
help_text="Size of this file in bytes", verbose_name="Size"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"kbitem",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="helpdesk.KBItem",
|
||||||
|
verbose_name="Knowledge base item",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Attachment',
|
"verbose_name": "Attachment",
|
||||||
'verbose_name_plural': 'Attachments',
|
"verbose_name_plural": "Attachments",
|
||||||
'ordering': ('filename',),
|
"ordering": ("filename",),
|
||||||
'abstract': False,
|
"abstract": False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.RenameModel(
|
migrations.RenameModel(
|
||||||
old_name='Attachment',
|
old_name="Attachment",
|
||||||
new_name='FollowUpAttachment',
|
new_name="FollowUpAttachment",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -6,66 +6,98 @@ import django.db.models.deletion
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
('helpdesk', '0026_kbitem_attachments'),
|
("helpdesk", "0026_kbitem_attachments"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='kbcategory',
|
model_name="kbcategory",
|
||||||
name='queue',
|
name="queue",
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='helpdesk.Queue', verbose_name='Default queue when creating a ticket after viewing this category.'),
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="helpdesk.Queue",
|
||||||
|
verbose_name="Default queue when creating a ticket after viewing this category.",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='kbitem',
|
model_name="kbitem",
|
||||||
name='downvoted_by',
|
name="downvoted_by",
|
||||||
field=models.ManyToManyField(related_name='downvotes', to=settings.AUTH_USER_MODEL),
|
field=models.ManyToManyField(
|
||||||
|
related_name="downvotes", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='ticket',
|
model_name="ticket",
|
||||||
name='kbitem',
|
name="kbitem",
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='helpdesk.KBItem', verbose_name='Knowledge base item the user was viewing when they created this ticket.'),
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="helpdesk.KBItem",
|
||||||
|
verbose_name="Knowledge base item the user was viewing when they created this ticket.",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='followupattachment',
|
model_name="followupattachment",
|
||||||
name='filename',
|
name="filename",
|
||||||
field=models.CharField(blank=True, max_length=1000, verbose_name='Filename'),
|
field=models.CharField(
|
||||||
|
blank=True, max_length=1000, verbose_name="Filename"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='followupattachment',
|
model_name="followupattachment",
|
||||||
name='mime_type',
|
name="mime_type",
|
||||||
field=models.CharField(blank=True, max_length=255, verbose_name='MIME Type'),
|
field=models.CharField(
|
||||||
|
blank=True, max_length=255, verbose_name="MIME Type"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='followupattachment',
|
model_name="followupattachment",
|
||||||
name='size',
|
name="size",
|
||||||
field=models.IntegerField(blank=True, help_text='Size of this file in bytes', verbose_name='Size'),
|
field=models.IntegerField(
|
||||||
|
blank=True, help_text="Size of this file in bytes", verbose_name="Size"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='kbiattachment',
|
model_name="kbiattachment",
|
||||||
name='filename',
|
name="filename",
|
||||||
field=models.CharField(blank=True, max_length=1000, verbose_name='Filename'),
|
field=models.CharField(
|
||||||
|
blank=True, max_length=1000, verbose_name="Filename"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='kbiattachment',
|
model_name="kbiattachment",
|
||||||
name='mime_type',
|
name="mime_type",
|
||||||
field=models.CharField(blank=True, max_length=255, verbose_name='MIME Type'),
|
field=models.CharField(
|
||||||
|
blank=True, max_length=255, verbose_name="MIME Type"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='kbiattachment',
|
model_name="kbiattachment",
|
||||||
name='size',
|
name="size",
|
||||||
field=models.IntegerField(blank=True, help_text='Size of this file in bytes', verbose_name='Size'),
|
field=models.IntegerField(
|
||||||
|
blank=True, help_text="Size of this file in bytes", verbose_name="Size"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='kbitem',
|
model_name="kbitem",
|
||||||
name='voted_by',
|
name="voted_by",
|
||||||
field=models.ManyToManyField(related_name='votes', to=settings.AUTH_USER_MODEL),
|
field=models.ManyToManyField(
|
||||||
|
related_name="votes", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='enable_notifications_on_email_events',
|
name="enable_notifications_on_email_events",
|
||||||
field=models.BooleanField(blank=True, default=False, help_text='When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature', verbose_name='Notify contacts when email updates arrive'),
|
field=models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
help_text="When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature",
|
||||||
|
verbose_name="Notify contacts when email updates arrive",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -7,15 +7,20 @@ from helpdesk import settings as helpdesk_settings
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0027_auto_20200107_1221'),
|
("helpdesk", "0027_auto_20200107_1221"),
|
||||||
] + helpdesk_settings.HELPDESK_TEAMS_MIGRATION_DEPENDENCIES
|
] + helpdesk_settings.HELPDESK_TEAMS_MIGRATION_DEPENDENCIES
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='kbitem',
|
model_name="kbitem",
|
||||||
name='team',
|
name="team",
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=helpdesk_settings.HELPDESK_TEAMS_MODEL, verbose_name='Team'),
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=helpdesk_settings.HELPDESK_TEAMS_MODEL,
|
||||||
|
verbose_name="Team",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -4,15 +4,16 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0028_kbitem_team'),
|
("helpdesk", "0028_kbitem_team"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='kbcategory',
|
model_name="kbcategory",
|
||||||
name='public',
|
name="public",
|
||||||
field=models.BooleanField(default=True, verbose_name='Is KBCategory publicly visible?'),
|
field=models.BooleanField(
|
||||||
|
default=True, verbose_name="Is KBCategory publicly visible?"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -2,32 +2,44 @@
|
|||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
def copy_title(apps, schema_editor):
|
def copy_title(apps, schema_editor):
|
||||||
KBCategory = apps.get_model("helpdesk", "KBCategory")
|
KBCategory = apps.get_model("helpdesk", "KBCategory")
|
||||||
KBCategory.objects.update(name=models.F('title'))
|
KBCategory.objects.update(name=models.F("title"))
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0029_kbcategory_public'),
|
("helpdesk", "0029_kbcategory_public"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='kbcategory',
|
model_name="kbcategory",
|
||||||
name='name',
|
name="name",
|
||||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Name of the category'),
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
max_length=100,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Name of the category",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='kbcategory',
|
model_name="kbcategory",
|
||||||
name='title',
|
name="title",
|
||||||
field=models.CharField(max_length=100, verbose_name='Title on knowledgebase page'),
|
field=models.CharField(
|
||||||
|
max_length=100, verbose_name="Title on knowledgebase page"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.RunPython(copy_title, migrations.RunPython.noop),
|
migrations.RunPython(copy_title, migrations.RunPython.noop),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='kbcategory',
|
model_name="kbcategory",
|
||||||
name='name',
|
name="name",
|
||||||
field=models.CharField(blank=False, max_length=100, null=False, verbose_name='Name of the category'),
|
field=models.CharField(
|
||||||
|
blank=False,
|
||||||
|
max_length=100,
|
||||||
|
null=False,
|
||||||
|
verbose_name="Name of the category",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -4,19 +4,24 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0030_add_kbcategory_name'),
|
("helpdesk", "0030_add_kbcategory_name"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='kbitem',
|
name="kbitem",
|
||||||
options={'ordering': ('order', 'title'), 'verbose_name': 'Knowledge base item', 'verbose_name_plural': 'Knowledge base items'},
|
options={
|
||||||
|
"ordering": ("order", "title"),
|
||||||
|
"verbose_name": "Knowledge base item",
|
||||||
|
"verbose_name_plural": "Knowledge base items",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='kbitem',
|
model_name="kbitem",
|
||||||
name='order',
|
name="order",
|
||||||
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Order'),
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True, null=True, verbose_name="Order"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -4,15 +4,16 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0031_auto_20200225_1440'),
|
("helpdesk", "0031_auto_20200225_1440"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='kbitem',
|
model_name="kbitem",
|
||||||
name='enabled',
|
name="enabled",
|
||||||
field=models.BooleanField(default=True, verbose_name='Enabled to display to users'),
|
field=models.BooleanField(
|
||||||
|
default=True, verbose_name="Enabled to display to users"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -5,15 +5,21 @@ import django.db.models.deletion
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0032_kbitem_enabled'),
|
("helpdesk", "0032_kbitem_enabled"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='ticket',
|
model_name="ticket",
|
||||||
name='merged_to',
|
name="merged_to",
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='merged_tickets', to='helpdesk.Ticket', verbose_name='merged to'),
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="merged_tickets",
|
||||||
|
to="helpdesk.Ticket",
|
||||||
|
verbose_name="merged to",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -7,10 +7,12 @@ def forwards_func(apps, schema_editor):
|
|||||||
EmailTemplate = apps.get_model("helpdesk", "EmailTemplate")
|
EmailTemplate = apps.get_model("helpdesk", "EmailTemplate")
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
EmailTemplate.objects.using(db_alias).create(
|
EmailTemplate.objects.using(db_alias).create(
|
||||||
id=EmailTemplate.objects.using(db_alias).order_by('-id').first().id + 1 if EmailTemplate.objects.using(db_alias).first() else 1, # because PG sequences are not reset
|
id=EmailTemplate.objects.using(db_alias).order_by("-id").first().id + 1
|
||||||
template_name='merged',
|
if EmailTemplate.objects.using(db_alias).first()
|
||||||
subject='(Merged)',
|
else 1, # because PG sequences are not reset
|
||||||
heading='Ticket merged',
|
template_name="merged",
|
||||||
|
subject="(Merged)",
|
||||||
|
heading="Ticket merged",
|
||||||
plain_text="""Hello,
|
plain_text="""Hello,
|
||||||
|
|
||||||
This is a courtesy e-mail to let you know that ticket {{ ticket.ticket }} ("{{ ticket.title }}") by {{ ticket.submitter_email }} has been merged to ticket {{ ticket.merged_to.ticket }}.
|
This is a courtesy e-mail to let you know that ticket {{ ticket.ticket }} ("{{ ticket.title }}") by {{ ticket.submitter_email }} has been merged to ticket {{ ticket.merged_to.ticket }}.
|
||||||
@ -21,13 +23,14 @@ From now on, please answer on this ticket, or you can include the tag {{ ticket.
|
|||||||
<p style="font-family: sans-serif; font-size: 1em;">This is a courtesy e-mail to let you know that ticket <b>{{ ticket.ticket }}</b> (<em>{{ ticket.title }}</em>) by {{ ticket.submitter_email }} has been merged to ticket <a href="{{ ticket.merged_to.staff_url }}">{{ ticket.merged_to.ticket }}</a>.</p>
|
<p style="font-family: sans-serif; font-size: 1em;">This is a courtesy e-mail to let you know that ticket <b>{{ ticket.ticket }}</b> (<em>{{ ticket.title }}</em>) by {{ ticket.submitter_email }} has been merged to ticket <a href="{{ ticket.merged_to.staff_url }}">{{ ticket.merged_to.ticket }}</a>.</p>
|
||||||
|
|
||||||
<p style="font-family: sans-serif; font-size: 1em;">From now on, please answer on this ticket, or you can include the tag <b>{{ ticket.merged_to.ticket }}</b> in your e-mail subject.</p>""",
|
<p style="font-family: sans-serif; font-size: 1em;">From now on, please answer on this ticket, or you can include the tag <b>{{ ticket.merged_to.ticket }}</b> in your e-mail subject.</p>""",
|
||||||
locale='en'
|
locale="en",
|
||||||
)
|
)
|
||||||
EmailTemplate.objects.using(db_alias).create(
|
EmailTemplate.objects.using(db_alias).create(
|
||||||
id=EmailTemplate.objects.using(db_alias).order_by('-id').first().id + 1, # because PG sequences are not reset
|
id=EmailTemplate.objects.using(db_alias).order_by("-id").first().id
|
||||||
template_name='merged',
|
+ 1, # because PG sequences are not reset
|
||||||
subject='(Fusionné)',
|
template_name="merged",
|
||||||
heading='Ticket Fusionné',
|
subject="(Fusionné)",
|
||||||
|
heading="Ticket Fusionné",
|
||||||
plain_text="""Bonjour,
|
plain_text="""Bonjour,
|
||||||
|
|
||||||
Ce courriel indicatif permet de vous prévenir que le ticket {{ ticket.ticket }} ("{{ ticket.title }}") par {{ ticket.submitter_email }} a été fusionné au ticket {{ ticket.merged_to.ticket }}.
|
Ce courriel indicatif permet de vous prévenir que le ticket {{ ticket.ticket }} ("{{ ticket.title }}") par {{ ticket.submitter_email }} a été fusionné au ticket {{ ticket.merged_to.ticket }}.
|
||||||
@ -38,20 +41,19 @@ Veillez à répondre sur ce ticket dorénavant, ou bien inclure la balise {{ tic
|
|||||||
<p style="font-family: sans-serif; font-size: 1em;">Ce courriel indicatif permet de vous prévenir que le ticket <b>{{ ticket.ticket }}</b> (<em>{{ ticket.title }}</em>) par {{ ticket.submitter_email }} a été fusionné au ticket <a href="{{ ticket.merged_to.staff_url }}">{{ ticket.merged_to.ticket }}</a>.</p>
|
<p style="font-family: sans-serif; font-size: 1em;">Ce courriel indicatif permet de vous prévenir que le ticket <b>{{ ticket.ticket }}</b> (<em>{{ ticket.title }}</em>) par {{ ticket.submitter_email }} a été fusionné au ticket <a href="{{ ticket.merged_to.staff_url }}">{{ ticket.merged_to.ticket }}</a>.</p>
|
||||||
|
|
||||||
<p style="font-family: sans-serif; font-size: 1em;">Veillez à répondre sur ce ticket dorénavant, ou bien inclure la balise <b>{{ ticket.merged_to.ticket }}</b> dans le sujet de votre réponse par mail.</p>""",
|
<p style="font-family: sans-serif; font-size: 1em;">Veillez à répondre sur ce ticket dorénavant, ou bien inclure la balise <b>{{ ticket.merged_to.ticket }}</b> dans le sujet de votre réponse par mail.</p>""",
|
||||||
locale='fr'
|
locale="fr",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def reverse_func(apps, schema_editor):
|
def reverse_func(apps, schema_editor):
|
||||||
EmailTemplate = apps.get_model("helpdesk", "EmailTemplate")
|
EmailTemplate = apps.get_model("helpdesk", "EmailTemplate")
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
EmailTemplate.objects.using(db_alias).filter(template_name='merged').delete()
|
EmailTemplate.objects.using(db_alias).filter(template_name="merged").delete()
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0033_ticket_merged_to'),
|
("helpdesk", "0033_ticket_merged_to"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -5,15 +5,18 @@ import helpdesk.models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("helpdesk", "0034_create_email_template_for_merged"),
|
||||||
|
]
|
||||||
|
|
||||||
dependencies = [
|
operations = [
|
||||||
('helpdesk', '0034_create_email_template_for_merged'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='usersettings',
|
model_name="usersettings",
|
||||||
name='email_on_ticket_change',
|
name="email_on_ticket_change",
|
||||||
field=models.BooleanField(default=helpdesk.models.email_on_ticket_change_default, help_text="If you're the ticket owner and the ticket is changed via the web by somebody else,do you want to receive an e-mail?", verbose_name='E-mail me on ticket change?'),
|
field=models.BooleanField(
|
||||||
|
default=helpdesk.models.email_on_ticket_change_default,
|
||||||
|
help_text="If you're the ticket owner and the ticket is changed via the web by somebody else,do you want to receive an e-mail?",
|
||||||
|
verbose_name="E-mail me on ticket change?",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -6,20 +6,29 @@ import helpdesk.validators
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0035_alter_email_on_ticket_change'),
|
("helpdesk", "0035_alter_email_on_ticket_change"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='followupattachment',
|
model_name="followupattachment",
|
||||||
name='file',
|
name="file",
|
||||||
field=models.FileField(max_length=1000, upload_to=helpdesk.models.attachment_path, validators=[helpdesk.validators.validate_file_extension], verbose_name='File'),
|
field=models.FileField(
|
||||||
|
max_length=1000,
|
||||||
|
upload_to=helpdesk.models.attachment_path,
|
||||||
|
validators=[helpdesk.validators.validate_file_extension],
|
||||||
|
verbose_name="File",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='kbiattachment',
|
model_name="kbiattachment",
|
||||||
name='file',
|
name="file",
|
||||||
field=models.FileField(max_length=1000, upload_to=helpdesk.models.attachment_path, validators=[helpdesk.validators.validate_file_extension], verbose_name='File'),
|
field=models.FileField(
|
||||||
|
max_length=1000,
|
||||||
|
upload_to=helpdesk.models.attachment_path,
|
||||||
|
validators=[helpdesk.validators.validate_file_extension],
|
||||||
|
verbose_name="File",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -4,15 +4,26 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0036_add_attachment_validator'),
|
("helpdesk", "0036_add_attachment_validator"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='queue',
|
model_name="queue",
|
||||||
name='email_box_type',
|
name="email_box_type",
|
||||||
field=models.CharField(blank=True, choices=[('pop3', 'POP 3'), ('imap', 'IMAP'), ('oauth', 'IMAP OAUTH'), ('local', 'Local Directory')], help_text='E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.', max_length=5, null=True, verbose_name='E-Mail Box Type'),
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("pop3", "POP 3"),
|
||||||
|
("imap", "IMAP"),
|
||||||
|
("oauth", "IMAP OAUTH"),
|
||||||
|
("local", "Local Directory"),
|
||||||
|
],
|
||||||
|
help_text="E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.",
|
||||||
|
max_length=5,
|
||||||
|
null=True,
|
||||||
|
verbose_name="E-Mail Box Type",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -6,49 +6,107 @@ import helpdesk.models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('helpdesk', '0037_alter_queue_email_box_type'),
|
("helpdesk", "0037_alter_queue_email_box_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Checklist',
|
name="Checklist",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('name', models.CharField(max_length=100, verbose_name='Name')),
|
"id",
|
||||||
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to='helpdesk.ticket', verbose_name='Ticket')),
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=100, verbose_name="Name")),
|
||||||
|
(
|
||||||
|
"ticket",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="checklists",
|
||||||
|
to="helpdesk.ticket",
|
||||||
|
verbose_name="Ticket",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Checklist',
|
"verbose_name": "Checklist",
|
||||||
'verbose_name_plural': 'Checklists',
|
"verbose_name_plural": "Checklists",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ChecklistTemplate',
|
name="ChecklistTemplate",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('name', models.CharField(max_length=100, verbose_name='Name')),
|
"id",
|
||||||
('task_list', models.JSONField(validators=[helpdesk.models.is_a_list_without_empty_element], verbose_name='Task List')),
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=100, verbose_name="Name")),
|
||||||
|
(
|
||||||
|
"task_list",
|
||||||
|
models.JSONField(
|
||||||
|
validators=[helpdesk.models.is_a_list_without_empty_element],
|
||||||
|
verbose_name="Task List",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Checklist Template',
|
"verbose_name": "Checklist Template",
|
||||||
'verbose_name_plural': 'Checklist Templates',
|
"verbose_name_plural": "Checklist Templates",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ChecklistTask',
|
name="ChecklistTask",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('description', models.CharField(max_length=250, verbose_name='Description')),
|
"id",
|
||||||
('completion_date', models.DateTimeField(blank=True, null=True, verbose_name='Completion Date')),
|
models.AutoField(
|
||||||
('position', models.PositiveSmallIntegerField(db_index=True, verbose_name='Position')),
|
auto_created=True,
|
||||||
('checklist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='helpdesk.checklist', verbose_name='Checklist')),
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"description",
|
||||||
|
models.CharField(max_length=250, verbose_name="Description"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"completion_date",
|
||||||
|
models.DateTimeField(
|
||||||
|
blank=True, null=True, verbose_name="Completion Date"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"position",
|
||||||
|
models.PositiveSmallIntegerField(
|
||||||
|
db_index=True, verbose_name="Position"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"checklist",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="tasks",
|
||||||
|
to="helpdesk.checklist",
|
||||||
|
verbose_name="Checklist",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Checklist Task',
|
"verbose_name": "Checklist Task",
|
||||||
'verbose_name_plural': 'Checklist Tasks',
|
"verbose_name_plural": "Checklist Tasks",
|
||||||
'ordering': ('position',),
|
"ordering": ("position",),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
1205
helpdesk/models.py
1205
helpdesk/models.py
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
from base64 import b64decode, b64encode
|
from base64 import b64decode, b64encode
|
||||||
from django.db.models import Q, Max
|
from django.db.models import Q, Max
|
||||||
from django.db.models import F, Window, Subquery, OuterRef
|
from django.db.models import F, Window, Subquery, OuterRef
|
||||||
@ -15,61 +14,61 @@ def query_to_base64(query):
|
|||||||
"""
|
"""
|
||||||
Converts a query dict object to a base64-encoded bytes object.
|
Converts a query dict object to a base64-encoded bytes object.
|
||||||
"""
|
"""
|
||||||
return b64encode(json.dumps(query).encode('UTF-8')).decode("ascii")
|
return b64encode(json.dumps(query).encode("UTF-8")).decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
def query_from_base64(b64data):
|
def query_from_base64(b64data):
|
||||||
"""
|
"""
|
||||||
Converts base64-encoded bytes object back to a query dict object.
|
Converts base64-encoded bytes object back to a query dict object.
|
||||||
"""
|
"""
|
||||||
query = {'search_string': ''}
|
query = {"search_string": ""}
|
||||||
query.update(json.loads(b64decode(b64data).decode('utf-8')))
|
query.update(json.loads(b64decode(b64data).decode("utf-8")))
|
||||||
if query['search_string'] is None:
|
if query["search_string"] is None:
|
||||||
query['search_string'] = ''
|
query["search_string"] = ""
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
def get_search_filter_args(search):
|
def get_search_filter_args(search):
|
||||||
if not search:
|
if not search:
|
||||||
return Q()
|
return Q()
|
||||||
if search.startswith('queue:'):
|
if search.startswith("queue:"):
|
||||||
return Q(queue__title__icontains=search[len('queue:'):])
|
return Q(queue__title__icontains=search[len("queue:") :])
|
||||||
if search.startswith('priority:'):
|
if search.startswith("priority:"):
|
||||||
return Q(priority__icontains=search[len('priority:'):])
|
return Q(priority__icontains=search[len("priority:") :])
|
||||||
my_filter = Q()
|
my_filter = Q()
|
||||||
for subsearch in search.split("OR"):
|
for subsearch in search.split("OR"):
|
||||||
subsearch = subsearch.strip()
|
subsearch = subsearch.strip()
|
||||||
if not subsearch:
|
if not subsearch:
|
||||||
continue
|
continue
|
||||||
my_filter = (
|
my_filter = (
|
||||||
filter |
|
filter
|
||||||
Q(id__icontains=subsearch) |
|
| Q(id__icontains=subsearch)
|
||||||
Q(title__icontains=subsearch) |
|
| Q(title__icontains=subsearch)
|
||||||
Q(description__icontains=subsearch) |
|
| Q(description__icontains=subsearch)
|
||||||
Q(priority__icontains=subsearch) |
|
| Q(priority__icontains=subsearch)
|
||||||
Q(resolution__icontains=subsearch) |
|
| Q(resolution__icontains=subsearch)
|
||||||
Q(submitter_email__icontains=subsearch) |
|
| Q(submitter_email__icontains=subsearch)
|
||||||
Q(assigned_to__email__icontains=subsearch) |
|
| Q(assigned_to__email__icontains=subsearch)
|
||||||
Q(ticketcustomfieldvalue__value__icontains=subsearch) |
|
| Q(ticketcustomfieldvalue__value__icontains=subsearch)
|
||||||
Q(created__icontains=subsearch) |
|
| Q(created__icontains=subsearch)
|
||||||
Q(due_date__icontains=subsearch)
|
| Q(due_date__icontains=subsearch)
|
||||||
)
|
)
|
||||||
return my_filter
|
return my_filter
|
||||||
|
|
||||||
|
|
||||||
DATATABLES_ORDER_COLUMN_CHOICES = Choices(
|
DATATABLES_ORDER_COLUMN_CHOICES = Choices(
|
||||||
('0', 'id'),
|
("0", "id"),
|
||||||
('1', 'title'),
|
("1", "title"),
|
||||||
('2', 'priority'),
|
("2", "priority"),
|
||||||
('3', 'queue'),
|
("3", "queue"),
|
||||||
('4', 'status'),
|
("4", "status"),
|
||||||
('5', 'created'),
|
("5", "created"),
|
||||||
('6', 'due_date'),
|
("6", "due_date"),
|
||||||
('7', 'assigned_to'),
|
("7", "assigned_to"),
|
||||||
('8', 'submitter_email'),
|
("8", "submitter_email"),
|
||||||
('9', 'last_followup'),
|
("9", "last_followup"),
|
||||||
# ('10', 'time_spent'),
|
# ('10', 'time_spent'),
|
||||||
('11', 'kbitem'),
|
("11", "kbitem"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -78,22 +77,19 @@ def get_query_class():
|
|||||||
|
|
||||||
def _get_query_class():
|
def _get_query_class():
|
||||||
return __Query__
|
return __Query__
|
||||||
return getattr(settings,
|
|
||||||
'HELPDESK_QUERY_CLASS',
|
return getattr(settings, "HELPDESK_QUERY_CLASS", _get_query_class)()
|
||||||
_get_query_class)()
|
|
||||||
|
|
||||||
|
|
||||||
class __Query__:
|
class __Query__:
|
||||||
def __init__(self, huser, base64query=None, query_params=None):
|
def __init__(self, huser, base64query=None, query_params=None):
|
||||||
self.huser = huser
|
self.huser = huser
|
||||||
self.params = query_params if query_params else query_from_base64(
|
self.params = query_params if query_params else query_from_base64(base64query)
|
||||||
base64query)
|
self.base64 = base64query if base64query else query_to_base64(query_params)
|
||||||
self.base64 = base64query if base64query else query_to_base64(
|
|
||||||
query_params)
|
|
||||||
self.result = None
|
self.result = None
|
||||||
|
|
||||||
def get_search_filter_args(self):
|
def get_search_filter_args(self):
|
||||||
search = self.params.get('search_string', '')
|
search = self.params.get("search_string", "")
|
||||||
return get_search_filter_args(search)
|
return get_search_filter_args(search)
|
||||||
|
|
||||||
def __run__(self, queryset):
|
def __run__(self, queryset):
|
||||||
@ -112,15 +108,15 @@ class __Query__:
|
|||||||
sorting: The name of the column to sort by
|
sorting: The name of the column to sort by
|
||||||
"""
|
"""
|
||||||
q_args = []
|
q_args = []
|
||||||
value_filters = self.params.get('filtering', {})
|
value_filters = self.params.get("filtering", {})
|
||||||
null_filters = self.params.get('filtering_null', {})
|
null_filters = self.params.get("filtering_null", {})
|
||||||
if null_filters:
|
if null_filters:
|
||||||
if value_filters:
|
if value_filters:
|
||||||
# Check if any of the value value_filters are for the same field as the
|
# Check if any of the value value_filters are for the same field as the
|
||||||
# ISNULL filter so that an OR filter can be set up
|
# ISNULL filter so that an OR filter can be set up
|
||||||
matched_null_keys = []
|
matched_null_keys = []
|
||||||
for null_key in null_filters:
|
for null_key in null_filters:
|
||||||
field_path = null_key[:-8] # Chop off the "__isnull"
|
field_path = null_key[:-8] # Chop off the "__isnull"
|
||||||
matched_key = None
|
matched_key = None
|
||||||
for val_key in value_filters:
|
for val_key in value_filters:
|
||||||
if val_key.startswith(field_path):
|
if val_key.startswith(field_path):
|
||||||
@ -140,10 +136,12 @@ class __Query__:
|
|||||||
for null_key in matched_null_keys:
|
for null_key in matched_null_keys:
|
||||||
del null_filters[null_key]
|
del null_filters[null_key]
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
*q_args, (Q(**value_filters) & Q(**null_filters)) & self.get_search_filter_args())
|
*q_args,
|
||||||
sorting = self.params.get('sorting', None)
|
(Q(**value_filters) & Q(**null_filters)) & self.get_search_filter_args(),
|
||||||
|
)
|
||||||
|
sorting = self.params.get("sorting", None)
|
||||||
if sorting:
|
if sorting:
|
||||||
sortreverse = self.params.get('sortreverse', None)
|
sortreverse = self.params.get("sortreverse", None)
|
||||||
if sortreverse:
|
if sortreverse:
|
||||||
sorting = "-%s" % sorting
|
sorting = "-%s" % sorting
|
||||||
queryset = queryset.order_by(sorting)
|
queryset = queryset.order_by(sorting)
|
||||||
@ -165,45 +163,49 @@ class __Query__:
|
|||||||
to a Serializer called DatatablesTicketSerializer in serializers.py.
|
to a Serializer called DatatablesTicketSerializer in serializers.py.
|
||||||
"""
|
"""
|
||||||
objects = self.get()
|
objects = self.get()
|
||||||
order_by = '-created'
|
order_by = "-created"
|
||||||
draw = int(kwargs.get('draw', [0])[0])
|
draw = int(kwargs.get("draw", [0])[0])
|
||||||
length = int(kwargs.get('length', [25])[0])
|
length = int(kwargs.get("length", [25])[0])
|
||||||
start = int(kwargs.get('start', [0])[0])
|
start = int(kwargs.get("start", [0])[0])
|
||||||
search_value = kwargs.get('search[value]', [""])[0]
|
search_value = kwargs.get("search[value]", [""])[0]
|
||||||
order_column = kwargs.get('order[0][column]', ['5'])[0]
|
order_column = kwargs.get("order[0][column]", ["5"])[0]
|
||||||
order = kwargs.get('order[0][dir]', ["asc"])[0]
|
order = kwargs.get("order[0][dir]", ["asc"])[0]
|
||||||
|
|
||||||
order_column = DATATABLES_ORDER_COLUMN_CHOICES[order_column]
|
order_column = DATATABLES_ORDER_COLUMN_CHOICES[order_column]
|
||||||
# django orm '-' -> desc
|
# django orm '-' -> desc
|
||||||
if order == 'desc':
|
if order == "desc":
|
||||||
order_column = '-' + order_column
|
order_column = "-" + order_column
|
||||||
|
|
||||||
queryset = objects.annotate(
|
queryset = objects.annotate(
|
||||||
last_followup=Subquery(
|
last_followup=Subquery(
|
||||||
FollowUp.objects.order_by().annotate(
|
FollowUp.objects.order_by()
|
||||||
|
.annotate(
|
||||||
last_followup=Window(
|
last_followup=Window(
|
||||||
expression=Max("date"),
|
expression=Max("date"),
|
||||||
partition_by=[F("ticket_id"),],
|
partition_by=[
|
||||||
order_by="-date"
|
F("ticket_id"),
|
||||||
|
],
|
||||||
|
order_by="-date",
|
||||||
)
|
)
|
||||||
).filter(
|
)
|
||||||
ticket_id=OuterRef("id")
|
.filter(ticket_id=OuterRef("id"))
|
||||||
).values("last_followup").distinct()
|
.values("last_followup")
|
||||||
|
.distinct()
|
||||||
)
|
)
|
||||||
).order_by(order_by)
|
).order_by(order_by)
|
||||||
|
|
||||||
total = queryset.count()
|
total = queryset.count()
|
||||||
|
|
||||||
if search_value: # Dead code currently
|
if search_value: # Dead code currently
|
||||||
queryset = queryset.filter(get_search_filter_args(search_value))
|
queryset = queryset.filter(get_search_filter_args(search_value))
|
||||||
|
|
||||||
count = queryset.count()
|
count = queryset.count()
|
||||||
queryset = queryset.order_by(order_column)[start:start + length]
|
queryset = queryset.order_by(order_column)[start : start + length]
|
||||||
return {
|
return {
|
||||||
'data': DatatablesTicketSerializer(queryset, many=True).data,
|
"data": DatatablesTicketSerializer(queryset, many=True).data,
|
||||||
'recordsFiltered': count,
|
"recordsFiltered": count,
|
||||||
'recordsTotal': total,
|
"recordsTotal": total,
|
||||||
'draw': draw
|
"draw": draw,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_timeline_context(self):
|
def get_timeline_context(self):
|
||||||
@ -212,33 +214,38 @@ class __Query__:
|
|||||||
for ticket in self.get():
|
for ticket in self.get():
|
||||||
for followup in ticket.followup_set.all():
|
for followup in ticket.followup_set.all():
|
||||||
event = {
|
event = {
|
||||||
'start_date': self.mk_timeline_date(followup.date),
|
"start_date": self.mk_timeline_date(followup.date),
|
||||||
'text': {
|
"text": {
|
||||||
'headline': ticket.title + ' - ' + followup.title,
|
"headline": ticket.title + " - " + followup.title,
|
||||||
'text': (
|
"text": (
|
||||||
(escape(followup.comment)
|
(
|
||||||
if followup.comment else _('No text'))
|
escape(followup.comment)
|
||||||
+
|
if followup.comment
|
||||||
'<br/> <a href="%s" class="btn" role="button">%s</a>'
|
else _("No text")
|
||||||
%
|
)
|
||||||
(reverse('helpdesk:view', kwargs={
|
+ '<br/> <a href="%s" class="btn" role="button">%s</a>'
|
||||||
'ticket_id': ticket.pk}), _("View ticket"))
|
% (
|
||||||
|
reverse(
|
||||||
|
"helpdesk:view", kwargs={"ticket_id": ticket.pk}
|
||||||
|
),
|
||||||
|
_("View ticket"),
|
||||||
|
)
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
'group': _('Messages'),
|
"group": _("Messages"),
|
||||||
}
|
}
|
||||||
events.append(event)
|
events.append(event)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'events': events,
|
"events": events,
|
||||||
}
|
}
|
||||||
|
|
||||||
def mk_timeline_date(self, date):
|
def mk_timeline_date(self, date):
|
||||||
return {
|
return {
|
||||||
'year': date.year,
|
"year": date.year,
|
||||||
'month': date.month,
|
"month": date.month,
|
||||||
'day': date.day,
|
"day": date.day,
|
||||||
'hour': date.hour,
|
"hour": date.hour,
|
||||||
'minute': date.minute,
|
"minute": date.minute,
|
||||||
'second': date.second,
|
"second": date.second,
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
|
|||||||
A serializer for the Ticket model, returns data in the format as required by
|
A serializer for the Ticket model, returns data in the format as required by
|
||||||
datatables for ticket_list.html. Called from staff.datatables_ticket_list.
|
datatables for ticket_list.html. Called from staff.datatables_ticket_list.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ticket = serializers.SerializerMethodField()
|
ticket = serializers.SerializerMethodField()
|
||||||
assigned_to = serializers.SerializerMethodField()
|
assigned_to = serializers.SerializerMethodField()
|
||||||
submitter = serializers.SerializerMethodField()
|
submitter = serializers.SerializerMethodField()
|
||||||
@ -30,9 +31,22 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Ticket
|
model = Ticket
|
||||||
# fields = '__all__'
|
# fields = '__all__'
|
||||||
fields = ('ticket', 'id', 'priority', 'title', 'queue', 'status',
|
fields = (
|
||||||
'created', 'due_date', 'assigned_to', 'submitter', 'last_followup',
|
"ticket",
|
||||||
'row_class', 'time_spent', 'kbitem')
|
"id",
|
||||||
|
"priority",
|
||||||
|
"title",
|
||||||
|
"queue",
|
||||||
|
"status",
|
||||||
|
"created",
|
||||||
|
"due_date",
|
||||||
|
"assigned_to",
|
||||||
|
"submitter",
|
||||||
|
"last_followup",
|
||||||
|
"row_class",
|
||||||
|
"time_spent",
|
||||||
|
"kbitem",
|
||||||
|
)
|
||||||
|
|
||||||
def get_queue(self, obj):
|
def get_queue(self, obj):
|
||||||
return {"title": obj.queue.title, "id": obj.queue.id}
|
return {"title": obj.queue.title, "id": obj.queue.id}
|
||||||
@ -71,39 +85,46 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
def get_kbitem(self, obj):
|
def get_kbitem(self, obj):
|
||||||
return obj.kbitem.title if obj.kbitem else ""
|
return obj.kbitem.title if obj.kbitem else ""
|
||||||
|
|
||||||
def get_last_followup(self, obj):
|
def get_last_followup(self, obj):
|
||||||
return obj.last_followup
|
return obj.last_followup
|
||||||
|
|
||||||
|
|
||||||
class FollowUpAttachmentSerializer(serializers.ModelSerializer):
|
class FollowUpAttachmentSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FollowUpAttachment
|
model = FollowUpAttachment
|
||||||
fields = ('id', 'followup', 'file', 'filename', 'mime_type', 'size')
|
fields = ("id", "followup", "file", "filename", "mime_type", "size")
|
||||||
|
|
||||||
|
|
||||||
class FollowUpSerializer(serializers.ModelSerializer):
|
class FollowUpSerializer(serializers.ModelSerializer):
|
||||||
followupattachment_set = FollowUpAttachmentSerializer(
|
followupattachment_set = FollowUpAttachmentSerializer(many=True, read_only=True)
|
||||||
many=True, read_only=True)
|
|
||||||
attachments = serializers.ListField(
|
attachments = serializers.ListField(
|
||||||
child=serializers.FileField(),
|
child=serializers.FileField(), write_only=True, required=False
|
||||||
write_only=True,
|
|
||||||
required=False
|
|
||||||
)
|
)
|
||||||
date = serializers.DateTimeField(read_only=True)
|
date = serializers.DateTimeField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FollowUp
|
model = FollowUp
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'ticket', 'user', 'title', 'comment', 'public', 'new_status',
|
"id",
|
||||||
'time_spent', 'attachments', 'followupattachment_set', 'date', 'message_id',
|
"ticket",
|
||||||
|
"user",
|
||||||
|
"title",
|
||||||
|
"comment",
|
||||||
|
"public",
|
||||||
|
"new_status",
|
||||||
|
"time_spent",
|
||||||
|
"attachments",
|
||||||
|
"followupattachment_set",
|
||||||
|
"date",
|
||||||
|
"message_id",
|
||||||
)
|
)
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
if validated_data["user"]:
|
if validated_data["user"]:
|
||||||
user = validated_data["user"]
|
user = validated_data["user"]
|
||||||
else:
|
else:
|
||||||
user = self.context['request'].user
|
user = self.context["request"].user
|
||||||
return update_ticket(
|
return update_ticket(
|
||||||
user=user,
|
user=user,
|
||||||
ticket=validated_data["ticket"],
|
ticket=validated_data["ticket"],
|
||||||
@ -121,12 +142,12 @@ class UserSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = get_user_model()
|
model = get_user_model()
|
||||||
fields = ('first_name', 'last_name', 'username', 'email', 'password')
|
fields = ("first_name", "last_name", "username", "email", "password")
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
user = super(UserSerializer, self).create(validated_data)
|
user = super(UserSerializer, self).create(validated_data)
|
||||||
user.is_active = True
|
user.is_active = True
|
||||||
user.set_password(validated_data['password'])
|
user.set_password(validated_data["password"])
|
||||||
user.save()
|
user.save()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@ -137,13 +158,14 @@ class BaseTicketSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
# Add custom fields
|
# Add custom fields
|
||||||
for field in CustomField.objects.all():
|
for field in CustomField.objects.all():
|
||||||
self.fields['custom_%s' % field.name] = field.build_api_field()
|
self.fields["custom_%s" % field.name] = field.build_api_field()
|
||||||
|
|
||||||
|
|
||||||
class PublicTicketListingSerializer(BaseTicketSerializer):
|
class PublicTicketListingSerializer(BaseTicketSerializer):
|
||||||
"""
|
"""
|
||||||
A serializer to be used by the public API for listing tickets. Don't expose private fields here!
|
A serializer to be used by the public API for listing tickets. Don't expose private fields here!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ticket = serializers.SerializerMethodField()
|
ticket = serializers.SerializerMethodField()
|
||||||
submitter = serializers.SerializerMethodField()
|
submitter = serializers.SerializerMethodField()
|
||||||
created = serializers.SerializerMethodField()
|
created = serializers.SerializerMethodField()
|
||||||
@ -156,8 +178,18 @@ class PublicTicketListingSerializer(BaseTicketSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Ticket
|
model = Ticket
|
||||||
# fields = '__all__'
|
# fields = '__all__'
|
||||||
fields = ('ticket', 'id', 'title', 'queue', 'status',
|
fields = (
|
||||||
'created', 'due_date', 'submitter', 'kbitem', 'secret_key')
|
"ticket",
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"queue",
|
||||||
|
"status",
|
||||||
|
"created",
|
||||||
|
"due_date",
|
||||||
|
"submitter",
|
||||||
|
"kbitem",
|
||||||
|
"secret_key",
|
||||||
|
)
|
||||||
|
|
||||||
def get_queue(self, obj):
|
def get_queue(self, obj):
|
||||||
return {"title": obj.queue.title, "id": obj.queue.id}
|
return {"title": obj.queue.title, "id": obj.queue.id}
|
||||||
@ -188,29 +220,40 @@ class TicketSerializer(BaseTicketSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Ticket
|
model = Ticket
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'queue', 'title', 'description', 'resolution', 'submitter_email', 'assigned_to', 'status', 'on_hold',
|
"id",
|
||||||
'priority', 'due_date', 'merged_to', 'attachment', 'followup_set'
|
"queue",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"resolution",
|
||||||
|
"submitter_email",
|
||||||
|
"assigned_to",
|
||||||
|
"status",
|
||||||
|
"on_hold",
|
||||||
|
"priority",
|
||||||
|
"due_date",
|
||||||
|
"merged_to",
|
||||||
|
"attachment",
|
||||||
|
"followup_set",
|
||||||
)
|
)
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
""" Use TicketForm to validate and create ticket """
|
"""Use TicketForm to validate and create ticket"""
|
||||||
queues = HelpdeskUser(self.context['request'].user).get_queues()
|
queues = HelpdeskUser(self.context["request"].user).get_queues()
|
||||||
queue_choices = [(q.id, q.title) for q in queues]
|
queue_choices = [(q.id, q.title) for q in queues]
|
||||||
data = validated_data.copy()
|
data = validated_data.copy()
|
||||||
data['body'] = data['description']
|
data["body"] = data["description"]
|
||||||
# TicketForm needs id for ForeignKey (not the instance themselves)
|
# TicketForm needs id for ForeignKey (not the instance themselves)
|
||||||
data['queue'] = data['queue'].id
|
data["queue"] = data["queue"].id
|
||||||
if data.get('assigned_to'):
|
if data.get("assigned_to"):
|
||||||
data['assigned_to'] = data['assigned_to'].id
|
data["assigned_to"] = data["assigned_to"].id
|
||||||
if data.get('merged_to'):
|
if data.get("merged_to"):
|
||||||
data['merged_to'] = data['merged_to'].id
|
data["merged_to"] = data["merged_to"].id
|
||||||
|
|
||||||
files = {'attachment': data.pop('attachment', None)}
|
files = {"attachment": data.pop("attachment", None)}
|
||||||
|
|
||||||
ticket_form = TicketForm(
|
ticket_form = TicketForm(data=data, files=files, queue_choices=queue_choices)
|
||||||
data=data, files=files, queue_choices=queue_choices)
|
|
||||||
if ticket_form.is_valid():
|
if ticket_form.is_valid():
|
||||||
ticket = ticket_form.save(user=self.context['request'].user)
|
ticket = ticket_form.save(user=self.context["request"].user)
|
||||||
ticket.set_custom_field_values()
|
ticket.set_custom_field_values()
|
||||||
return ticket
|
return ticket
|
||||||
|
|
||||||
|
@ -14,11 +14,11 @@ import sys
|
|||||||
|
|
||||||
|
|
||||||
DEFAULT_USER_SETTINGS = {
|
DEFAULT_USER_SETTINGS = {
|
||||||
'login_view_ticketlist': True,
|
"login_view_ticketlist": True,
|
||||||
'email_on_ticket_change': True,
|
"email_on_ticket_change": True,
|
||||||
'email_on_ticket_assign': True,
|
"email_on_ticket_assign": True,
|
||||||
'tickets_per_page': 25,
|
"tickets_per_page": 25,
|
||||||
'use_email_as_submitter': True,
|
"use_email_as_submitter": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -33,8 +33,8 @@ HAS_TAG_SUPPORT = False
|
|||||||
USE_TZ: bool = True
|
USE_TZ: bool = True
|
||||||
|
|
||||||
# check for secure cookie support
|
# check for secure cookie support
|
||||||
if os.environ.get('SECURE_PROXY_SSL_HEADER'):
|
if os.environ.get("SECURE_PROXY_SSL_HEADER"):
|
||||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||||
|
|
||||||
SESSION_COOKIE_SECURE = True
|
SESSION_COOKIE_SECURE = True
|
||||||
CSRF_COOKIE_SECURE = True
|
CSRF_COOKIE_SECURE = True
|
||||||
@ -44,130 +44,168 @@ if os.environ.get('SECURE_PROXY_SSL_HEADER'):
|
|||||||
##########################################
|
##########################################
|
||||||
|
|
||||||
# redirect to login page instead of the default homepage when users visits "/"?
|
# redirect to login page instead of the default homepage when users visits "/"?
|
||||||
HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(settings,
|
HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(
|
||||||
'HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT',
|
settings, "HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT", False
|
||||||
False)
|
)
|
||||||
|
|
||||||
HELPDESK_PUBLIC_VIEW_PROTECTOR = getattr(settings,
|
HELPDESK_PUBLIC_VIEW_PROTECTOR = getattr(
|
||||||
'HELPDESK_PUBLIC_VIEW_PROTECTOR',
|
settings, "HELPDESK_PUBLIC_VIEW_PROTECTOR", lambda _: None
|
||||||
lambda _: None)
|
)
|
||||||
|
|
||||||
HELPDESK_STAFF_VIEW_PROTECTOR = getattr(settings,
|
HELPDESK_STAFF_VIEW_PROTECTOR = getattr(
|
||||||
'HELPDESK_STAFF_VIEW_PROTECTOR',
|
settings, "HELPDESK_STAFF_VIEW_PROTECTOR", lambda _: None
|
||||||
lambda _: None)
|
)
|
||||||
|
|
||||||
# Enable ticket and Email attachments
|
# Enable ticket and Email attachments
|
||||||
#
|
#
|
||||||
# Caution! Set this to False, unless you have secured access to
|
# Caution! Set this to False, unless you have secured access to
|
||||||
# the uploaded files. Otherwise anyone on the Internet will be
|
# the uploaded files. Otherwise anyone on the Internet will be
|
||||||
# able to download your ticket attachments.
|
# able to download your ticket attachments.
|
||||||
HELPDESK_ENABLE_ATTACHMENTS = getattr(settings,
|
HELPDESK_ENABLE_ATTACHMENTS = getattr(settings, "HELPDESK_ENABLE_ATTACHMENTS", True)
|
||||||
'HELPDESK_ENABLE_ATTACHMENTS',
|
|
||||||
True)
|
|
||||||
|
|
||||||
# Enable the Dependencies field on ticket view
|
# Enable the Dependencies field on ticket view
|
||||||
HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(settings,
|
HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(
|
||||||
'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET',
|
settings, "HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET", True
|
||||||
True)
|
)
|
||||||
|
|
||||||
# Enable the Time spent on field on ticket view
|
# Enable the Time spent on field on ticket view
|
||||||
HELPDESK_ENABLE_TIME_SPENT_ON_TICKET = getattr(settings,
|
HELPDESK_ENABLE_TIME_SPENT_ON_TICKET = getattr(
|
||||||
'HELPDESK_ENABLE_TIME_SPENT_ON_TICKET',
|
settings, "HELPDESK_ENABLE_TIME_SPENT_ON_TICKET", True
|
||||||
True)
|
)
|
||||||
|
|
||||||
# raises a 404 to anon users. It's like it was invisible
|
# raises a 404 to anon users. It's like it was invisible
|
||||||
HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings,
|
HELPDESK_ANON_ACCESS_RAISES_404 = getattr(
|
||||||
'HELPDESK_ANON_ACCESS_RAISES_404',
|
settings, "HELPDESK_ANON_ACCESS_RAISES_404", False
|
||||||
False)
|
)
|
||||||
|
|
||||||
# Disable Timeline on ticket list
|
# Disable Timeline on ticket list
|
||||||
HELPDESK_TICKETS_TIMELINE_ENABLED = getattr(
|
HELPDESK_TICKETS_TIMELINE_ENABLED = getattr(
|
||||||
settings, 'HELPDESK_TICKETS_TIMELINE_ENABLED', True)
|
settings, "HELPDESK_TICKETS_TIMELINE_ENABLED", True
|
||||||
|
)
|
||||||
|
|
||||||
# show extended navigation by default, to all users, irrespective of staff
|
# show extended navigation by default, to all users, irrespective of staff
|
||||||
# status?
|
# status?
|
||||||
HELPDESK_NAVIGATION_ENABLED = getattr(
|
HELPDESK_NAVIGATION_ENABLED = getattr(settings, "HELPDESK_NAVIGATION_ENABLED", False)
|
||||||
settings, 'HELPDESK_NAVIGATION_ENABLED', False)
|
|
||||||
|
|
||||||
# use public CDNs to serve jquery and other javascript by default?
|
# use public CDNs to serve jquery and other javascript by default?
|
||||||
# otherwise, use built-in static copy
|
# otherwise, use built-in static copy
|
||||||
HELPDESK_USE_CDN = getattr(settings, 'HELPDESK_USE_CDN', False)
|
HELPDESK_USE_CDN = getattr(settings, "HELPDESK_USE_CDN", False)
|
||||||
|
|
||||||
# show dropdown list of languages that ticket comments can be translated into?
|
# show dropdown list of languages that ticket comments can be translated into?
|
||||||
HELPDESK_TRANSLATE_TICKET_COMMENTS = getattr(settings,
|
HELPDESK_TRANSLATE_TICKET_COMMENTS = getattr(
|
||||||
'HELPDESK_TRANSLATE_TICKET_COMMENTS',
|
settings, "HELPDESK_TRANSLATE_TICKET_COMMENTS", False
|
||||||
False)
|
)
|
||||||
|
|
||||||
# list of languages to offer. if set to false,
|
# list of languages to offer. if set to false,
|
||||||
# all default google translate languages will be shown.
|
# all default google translate languages will be shown.
|
||||||
HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG = getattr(settings,
|
HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG = getattr(
|
||||||
'HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG',
|
settings,
|
||||||
["en", "de", "es", "fr", "it", "ru"])
|
"HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG",
|
||||||
|
["en", "de", "es", "fr", "it", "ru"],
|
||||||
|
)
|
||||||
|
|
||||||
# show link to 'change password' on 'User Settings' page?
|
# show link to 'change password' on 'User Settings' page?
|
||||||
HELPDESK_SHOW_CHANGE_PASSWORD = getattr(
|
HELPDESK_SHOW_CHANGE_PASSWORD = getattr(
|
||||||
settings, 'HELPDESK_SHOW_CHANGE_PASSWORD', False)
|
settings, "HELPDESK_SHOW_CHANGE_PASSWORD", False
|
||||||
|
)
|
||||||
|
|
||||||
# allow user to override default layout for 'followups' - work in progress.
|
# allow user to override default layout for 'followups' - work in progress.
|
||||||
HELPDESK_FOLLOWUP_MOD = getattr(settings, 'HELPDESK_FOLLOWUP_MOD', False)
|
HELPDESK_FOLLOWUP_MOD = getattr(settings, "HELPDESK_FOLLOWUP_MOD", False)
|
||||||
|
|
||||||
# auto-subscribe user to ticket if (s)he responds to a ticket?
|
# auto-subscribe user to ticket if (s)he responds to a ticket?
|
||||||
HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE = getattr(settings,
|
HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE = getattr(
|
||||||
'HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE',
|
settings, "HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE", False
|
||||||
False)
|
)
|
||||||
|
|
||||||
# URL schemes that are allowed within links
|
# URL schemes that are allowed within links
|
||||||
ALLOWED_URL_SCHEMES = getattr(settings, 'ALLOWED_URL_SCHEMES', (
|
ALLOWED_URL_SCHEMES = getattr(
|
||||||
'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp',
|
settings,
|
||||||
))
|
"ALLOWED_URL_SCHEMES",
|
||||||
|
(
|
||||||
|
"file",
|
||||||
|
"ftp",
|
||||||
|
"ftps",
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"irc",
|
||||||
|
"mailto",
|
||||||
|
"sftp",
|
||||||
|
"ssh",
|
||||||
|
"tel",
|
||||||
|
"telnet",
|
||||||
|
"tftp",
|
||||||
|
"vnc",
|
||||||
|
"xmpp",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# Ticket status choices
|
# Ticket status choices
|
||||||
OPEN_STATUS = getattr(settings, 'HELPDESK_TICKET_OPEN_STATUS', 1)
|
OPEN_STATUS = getattr(settings, "HELPDESK_TICKET_OPEN_STATUS", 1)
|
||||||
REOPENED_STATUS = getattr(settings, 'HELPDESK_TICKET_REOPENED_STATUS', 2)
|
REOPENED_STATUS = getattr(settings, "HELPDESK_TICKET_REOPENED_STATUS", 2)
|
||||||
RESOLVED_STATUS = getattr(settings, 'HELPDESK_TICKET_RESOLVED_STATUS', 3)
|
RESOLVED_STATUS = getattr(settings, "HELPDESK_TICKET_RESOLVED_STATUS", 3)
|
||||||
CLOSED_STATUS = getattr(settings, 'HELPDESK_TICKET_CLOSED_STATUS', 4)
|
CLOSED_STATUS = getattr(settings, "HELPDESK_TICKET_CLOSED_STATUS", 4)
|
||||||
DUPLICATE_STATUS = getattr(settings, 'HELPDESK_TICKET_DUPLICATE_STATUS', 5)
|
DUPLICATE_STATUS = getattr(settings, "HELPDESK_TICKET_DUPLICATE_STATUS", 5)
|
||||||
|
|
||||||
DEFAULT_TICKET_STATUS_CHOICES = (
|
DEFAULT_TICKET_STATUS_CHOICES = (
|
||||||
(OPEN_STATUS, _('Open')),
|
(OPEN_STATUS, _("Open")),
|
||||||
(REOPENED_STATUS, _('Reopened')),
|
(REOPENED_STATUS, _("Reopened")),
|
||||||
(RESOLVED_STATUS, _('Resolved')),
|
(RESOLVED_STATUS, _("Resolved")),
|
||||||
(CLOSED_STATUS, _('Closed')),
|
(CLOSED_STATUS, _("Closed")),
|
||||||
(DUPLICATE_STATUS, _('Duplicate')),
|
(DUPLICATE_STATUS, _("Duplicate")),
|
||||||
|
)
|
||||||
|
TICKET_STATUS_CHOICES = getattr(
|
||||||
|
settings, "HELPDESK_TICKET_STATUS_CHOICES", DEFAULT_TICKET_STATUS_CHOICES
|
||||||
)
|
)
|
||||||
TICKET_STATUS_CHOICES = getattr(settings,
|
|
||||||
'HELPDESK_TICKET_STATUS_CHOICES',
|
|
||||||
DEFAULT_TICKET_STATUS_CHOICES)
|
|
||||||
|
|
||||||
# List of status choices considered as "open"
|
# List of status choices considered as "open"
|
||||||
DEFAULT_TICKET_OPEN_STATUSES = (OPEN_STATUS, REOPENED_STATUS)
|
DEFAULT_TICKET_OPEN_STATUSES = (OPEN_STATUS, REOPENED_STATUS)
|
||||||
TICKET_OPEN_STATUSES = getattr(settings,
|
TICKET_OPEN_STATUSES = getattr(
|
||||||
'HELPDESK_TICKET_OPEN_STATUSES',
|
settings, "HELPDESK_TICKET_OPEN_STATUSES", DEFAULT_TICKET_OPEN_STATUSES
|
||||||
DEFAULT_TICKET_OPEN_STATUSES)
|
)
|
||||||
|
|
||||||
# New status list choices depending on current ticket status
|
# New status list choices depending on current ticket status
|
||||||
DEFAULT_TICKET_STATUS_CHOICES_FLOW = {
|
DEFAULT_TICKET_STATUS_CHOICES_FLOW = {
|
||||||
OPEN_STATUS: (OPEN_STATUS, RESOLVED_STATUS, CLOSED_STATUS, DUPLICATE_STATUS,),
|
OPEN_STATUS: (
|
||||||
REOPENED_STATUS: (REOPENED_STATUS, RESOLVED_STATUS, CLOSED_STATUS, DUPLICATE_STATUS,),
|
OPEN_STATUS,
|
||||||
RESOLVED_STATUS: (REOPENED_STATUS, RESOLVED_STATUS, CLOSED_STATUS,),
|
RESOLVED_STATUS,
|
||||||
CLOSED_STATUS: (REOPENED_STATUS, CLOSED_STATUS,),
|
CLOSED_STATUS,
|
||||||
DUPLICATE_STATUS: (REOPENED_STATUS, DUPLICATE_STATUS,),
|
DUPLICATE_STATUS,
|
||||||
|
),
|
||||||
|
REOPENED_STATUS: (
|
||||||
|
REOPENED_STATUS,
|
||||||
|
RESOLVED_STATUS,
|
||||||
|
CLOSED_STATUS,
|
||||||
|
DUPLICATE_STATUS,
|
||||||
|
),
|
||||||
|
RESOLVED_STATUS: (
|
||||||
|
REOPENED_STATUS,
|
||||||
|
RESOLVED_STATUS,
|
||||||
|
CLOSED_STATUS,
|
||||||
|
),
|
||||||
|
CLOSED_STATUS: (
|
||||||
|
REOPENED_STATUS,
|
||||||
|
CLOSED_STATUS,
|
||||||
|
),
|
||||||
|
DUPLICATE_STATUS: (
|
||||||
|
REOPENED_STATUS,
|
||||||
|
DUPLICATE_STATUS,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
TICKET_STATUS_CHOICES_FLOW = getattr(settings,
|
TICKET_STATUS_CHOICES_FLOW = getattr(
|
||||||
'HELPDESK_TICKET_STATUS_CHOICES_FLOW',
|
settings, "HELPDESK_TICKET_STATUS_CHOICES_FLOW", DEFAULT_TICKET_STATUS_CHOICES_FLOW
|
||||||
DEFAULT_TICKET_STATUS_CHOICES_FLOW)
|
)
|
||||||
|
|
||||||
# Ticket priority choices
|
# Ticket priority choices
|
||||||
DEFAULT_TICKET_PRIORITY_CHOICES = (
|
DEFAULT_TICKET_PRIORITY_CHOICES = (
|
||||||
(1, _('1. Critical')),
|
(1, _("1. Critical")),
|
||||||
(2, _('2. High')),
|
(2, _("2. High")),
|
||||||
(3, _('3. Normal')),
|
(3, _("3. Normal")),
|
||||||
(4, _('4. Low')),
|
(4, _("4. Low")),
|
||||||
(5, _('5. Very Low')),
|
(5, _("5. Very Low")),
|
||||||
|
)
|
||||||
|
TICKET_PRIORITY_CHOICES = getattr(
|
||||||
|
settings, "HELPDESK_TICKET_PRIORITY_CHOICES", DEFAULT_TICKET_PRIORITY_CHOICES
|
||||||
)
|
)
|
||||||
TICKET_PRIORITY_CHOICES = getattr(settings,
|
|
||||||
'HELPDESK_TICKET_PRIORITY_CHOICES',
|
|
||||||
DEFAULT_TICKET_PRIORITY_CHOICES)
|
|
||||||
|
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
@ -175,59 +213,55 @@ TICKET_PRIORITY_CHOICES = getattr(settings,
|
|||||||
#########################
|
#########################
|
||||||
|
|
||||||
# Follow-ups automatic time_spent calculation
|
# Follow-ups automatic time_spent calculation
|
||||||
FOLLOWUP_TIME_SPENT_AUTO = getattr(settings,
|
FOLLOWUP_TIME_SPENT_AUTO = getattr(settings, "HELPDESK_FOLLOWUP_TIME_SPENT_AUTO", False)
|
||||||
'HELPDESK_FOLLOWUP_TIME_SPENT_AUTO',
|
|
||||||
False)
|
|
||||||
|
|
||||||
# Calculate time_spent according to open hours
|
# Calculate time_spent according to open hours
|
||||||
FOLLOWUP_TIME_SPENT_OPENING_HOURS = getattr(settings,
|
FOLLOWUP_TIME_SPENT_OPENING_HOURS = getattr(
|
||||||
'HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS',
|
settings, "HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS", {}
|
||||||
{})
|
)
|
||||||
|
|
||||||
# Holidays don't count for time_spent calculation
|
# Holidays don't count for time_spent calculation
|
||||||
FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = getattr(settings,
|
FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = getattr(
|
||||||
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS',
|
settings, "HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS", ()
|
||||||
())
|
)
|
||||||
|
|
||||||
# Time doesn't count for listed ticket statuses
|
# Time doesn't count for listed ticket statuses
|
||||||
FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = getattr(settings,
|
FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = getattr(
|
||||||
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES',
|
settings, "HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES", ()
|
||||||
())
|
)
|
||||||
|
|
||||||
# Time doesn't count for listed queues slugs
|
# Time doesn't count for listed queues slugs
|
||||||
FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = getattr(settings,
|
FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = getattr(
|
||||||
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES',
|
settings, "HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES", ()
|
||||||
())
|
)
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# options for public pages #
|
# options for public pages #
|
||||||
############################
|
############################
|
||||||
|
|
||||||
# show 'view a ticket' section on public page?
|
# show 'view a ticket' section on public page?
|
||||||
HELPDESK_VIEW_A_TICKET_PUBLIC = getattr(
|
HELPDESK_VIEW_A_TICKET_PUBLIC = getattr(settings, "HELPDESK_VIEW_A_TICKET_PUBLIC", True)
|
||||||
settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC', True)
|
|
||||||
|
|
||||||
# show 'submit a ticket' section on public page?
|
# show 'submit a ticket' section on public page?
|
||||||
HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(
|
HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(
|
||||||
settings, 'HELPDESK_SUBMIT_A_TICKET_PUBLIC', True)
|
settings, "HELPDESK_SUBMIT_A_TICKET_PUBLIC", True
|
||||||
|
)
|
||||||
|
|
||||||
# change that to custom class to have extra fields or validation (like captcha)
|
# change that to custom class to have extra fields or validation (like captcha)
|
||||||
HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr(
|
HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr(
|
||||||
settings,
|
settings, "HELPDESK_PUBLIC_TICKET_FORM_CLASS", "helpdesk.forms.PublicTicketForm"
|
||||||
"HELPDESK_PUBLIC_TICKET_FORM_CLASS",
|
|
||||||
"helpdesk.forms.PublicTicketForm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Custom fields constants
|
# Custom fields constants
|
||||||
CUSTOMFIELD_TO_FIELD_DICT = {
|
CUSTOMFIELD_TO_FIELD_DICT = {
|
||||||
'boolean': forms.BooleanField,
|
"boolean": forms.BooleanField,
|
||||||
'date': forms.DateField,
|
"date": forms.DateField,
|
||||||
'time': forms.TimeField,
|
"time": forms.TimeField,
|
||||||
'datetime': forms.DateTimeField,
|
"datetime": forms.DateTimeField,
|
||||||
'email': forms.EmailField,
|
"email": forms.EmailField,
|
||||||
'url': forms.URLField,
|
"url": forms.URLField,
|
||||||
'ipaddress': forms.GenericIPAddressField,
|
"ipaddress": forms.GenericIPAddressField,
|
||||||
'slug': forms.SlugField,
|
"slug": forms.SlugField,
|
||||||
}
|
}
|
||||||
CUSTOMFIELD_DATE_FORMAT = "%Y-%m-%d"
|
CUSTOMFIELD_DATE_FORMAT = "%Y-%m-%d"
|
||||||
CUSTOMFIELD_TIME_FORMAT = "%H:%M:%S"
|
CUSTOMFIELD_TIME_FORMAT = "%H:%M:%S"
|
||||||
@ -238,48 +272,58 @@ CUSTOMFIELD_DATETIME_FORMAT = f"{CUSTOMFIELD_DATE_FORMAT}T%H:%M"
|
|||||||
# options for update_ticket views #
|
# options for update_ticket views #
|
||||||
###################################
|
###################################
|
||||||
|
|
||||||
''' options for update_ticket views '''
|
""" options for update_ticket views """
|
||||||
# allow non-staff users to interact with tickets?
|
# allow non-staff users to interact with tickets?
|
||||||
# can be True/False or a callable accepting the active user and returning
|
# can be True/False or a callable accepting the active user and returning
|
||||||
# True if they must be considered helpdesk staff
|
# True if they must be considered helpdesk staff
|
||||||
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(
|
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(
|
||||||
settings, 'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE', False)
|
settings, "HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE", False
|
||||||
if not (HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE in (True, False) or callable(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE)):
|
)
|
||||||
|
if not (
|
||||||
|
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE in (True, False)
|
||||||
|
or callable(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE)
|
||||||
|
):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE should be set to either True/False or a callable.",
|
"HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE should be set to either True/False or a callable.",
|
||||||
RuntimeWarning
|
RuntimeWarning,
|
||||||
)
|
)
|
||||||
|
|
||||||
# show edit buttons in ticket follow ups.
|
# show edit buttons in ticket follow ups.
|
||||||
HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(settings,
|
HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(
|
||||||
'HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP',
|
settings, "HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP", True
|
||||||
True)
|
)
|
||||||
|
|
||||||
HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST = getattr(settings,
|
HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST = getattr(
|
||||||
'HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST',
|
settings, "HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST", []
|
||||||
[])
|
)
|
||||||
|
|
||||||
# show delete buttons in ticket follow ups if user is 'superuser'
|
# show delete buttons in ticket follow ups if user is 'superuser'
|
||||||
HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP = getattr(
|
HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP = getattr(
|
||||||
settings, 'HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP', False)
|
settings, "HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP", False
|
||||||
|
)
|
||||||
|
|
||||||
# make all updates public by default? this will hide the 'is this update
|
# make all updates public by default? this will hide the 'is this update
|
||||||
# public' checkbox
|
# public' checkbox
|
||||||
HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr(
|
HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr(
|
||||||
settings, 'HELPDESK_UPDATE_PUBLIC_DEFAULT', False)
|
settings, "HELPDESK_UPDATE_PUBLIC_DEFAULT", False
|
||||||
|
)
|
||||||
|
|
||||||
# only show staff users in ticket owner drop-downs
|
# only show staff users in ticket owner drop-downs
|
||||||
HELPDESK_STAFF_ONLY_TICKET_OWNERS = getattr(
|
HELPDESK_STAFF_ONLY_TICKET_OWNERS = getattr(
|
||||||
settings, 'HELPDESK_STAFF_ONLY_TICKET_OWNERS', False)
|
settings, "HELPDESK_STAFF_ONLY_TICKET_OWNERS", False
|
||||||
|
)
|
||||||
|
|
||||||
# only show staff users in ticket cc drop-down
|
# only show staff users in ticket cc drop-down
|
||||||
HELPDESK_STAFF_ONLY_TICKET_CC = getattr(
|
HELPDESK_STAFF_ONLY_TICKET_CC = getattr(
|
||||||
settings, 'HELPDESK_STAFF_ONLY_TICKET_CC', False)
|
settings, "HELPDESK_STAFF_ONLY_TICKET_CC", False
|
||||||
|
)
|
||||||
|
|
||||||
# allow the subject to have a configurable template.
|
# allow the subject to have a configurable template.
|
||||||
HELPDESK_EMAIL_SUBJECT_TEMPLATE = getattr(
|
HELPDESK_EMAIL_SUBJECT_TEMPLATE = getattr(
|
||||||
settings, 'HELPDESK_EMAIL_SUBJECT_TEMPLATE',
|
settings,
|
||||||
"{{ ticket.ticket }} {{ ticket.title|safe }} %(subject)s")
|
"HELPDESK_EMAIL_SUBJECT_TEMPLATE",
|
||||||
|
"{{ ticket.ticket }} {{ ticket.title|safe }} %(subject)s",
|
||||||
|
)
|
||||||
# since django-helpdesk may not work correctly without the ticket ID
|
# since django-helpdesk may not work correctly without the ticket ID
|
||||||
# in the subject, let's do a check for it quick:
|
# in the subject, let's do a check for it quick:
|
||||||
if HELPDESK_EMAIL_SUBJECT_TEMPLATE.find("ticket.ticket") < 0:
|
if HELPDESK_EMAIL_SUBJECT_TEMPLATE.find("ticket.ticket") < 0:
|
||||||
@ -287,12 +331,14 @@ if HELPDESK_EMAIL_SUBJECT_TEMPLATE.find("ticket.ticket") < 0:
|
|||||||
|
|
||||||
# default fallback locale when queue locale not found
|
# default fallback locale when queue locale not found
|
||||||
HELPDESK_EMAIL_FALLBACK_LOCALE = getattr(
|
HELPDESK_EMAIL_FALLBACK_LOCALE = getattr(
|
||||||
settings, 'HELPDESK_EMAIL_FALLBACK_LOCALE', 'en')
|
settings, "HELPDESK_EMAIL_FALLBACK_LOCALE", "en"
|
||||||
|
)
|
||||||
|
|
||||||
# default maximum email attachment size, in bytes
|
# default maximum email attachment size, in bytes
|
||||||
# only attachments smaller than this size will be sent via email
|
# only attachments smaller than this size will be sent via email
|
||||||
HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr(
|
HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr(
|
||||||
settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000)
|
settings, "HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE", 512000
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
@ -301,7 +347,8 @@ HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr(
|
|||||||
|
|
||||||
# hide the 'assigned to' / 'Case owner' field from the 'create_ticket' view?
|
# hide the 'assigned to' / 'Case owner' field from the 'create_ticket' view?
|
||||||
HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr(
|
HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr(
|
||||||
settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False)
|
settings, "HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO", False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#################
|
#################
|
||||||
@ -309,33 +356,37 @@ HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr(
|
|||||||
#################
|
#################
|
||||||
|
|
||||||
# default Queue email submission settings
|
# default Queue email submission settings
|
||||||
QUEUE_EMAIL_BOX_TYPE = getattr(settings, 'QUEUE_EMAIL_BOX_TYPE', None)
|
QUEUE_EMAIL_BOX_TYPE = getattr(settings, "QUEUE_EMAIL_BOX_TYPE", None)
|
||||||
QUEUE_EMAIL_BOX_SSL = getattr(settings, 'QUEUE_EMAIL_BOX_SSL', None)
|
QUEUE_EMAIL_BOX_SSL = getattr(settings, "QUEUE_EMAIL_BOX_SSL", None)
|
||||||
QUEUE_EMAIL_BOX_HOST = getattr(settings, 'QUEUE_EMAIL_BOX_HOST', None)
|
QUEUE_EMAIL_BOX_HOST = getattr(settings, "QUEUE_EMAIL_BOX_HOST", None)
|
||||||
QUEUE_EMAIL_BOX_USER = getattr(settings, 'QUEUE_EMAIL_BOX_USER', None)
|
QUEUE_EMAIL_BOX_USER = getattr(settings, "QUEUE_EMAIL_BOX_USER", None)
|
||||||
QUEUE_EMAIL_BOX_PASSWORD = getattr(settings, 'QUEUE_EMAIL_BOX_PASSWORD', None)
|
QUEUE_EMAIL_BOX_PASSWORD = getattr(settings, "QUEUE_EMAIL_BOX_PASSWORD", None)
|
||||||
|
|
||||||
# only process emails with a valid tracking ID? (throws away all other mail)
|
# only process emails with a valid tracking ID? (throws away all other mail)
|
||||||
QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr(
|
QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr(settings, "QUEUE_EMAIL_BOX_UPDATE_ONLY", False)
|
||||||
settings, 'QUEUE_EMAIL_BOX_UPDATE_ONLY', False)
|
|
||||||
|
|
||||||
# only allow users to access queues that they are members of?
|
# only allow users to access queues that they are members of?
|
||||||
HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = getattr(
|
HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = getattr(
|
||||||
settings, 'HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION', False)
|
settings, "HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION", False
|
||||||
|
)
|
||||||
|
|
||||||
# use https in the email links
|
# use https in the email links
|
||||||
HELPDESK_USE_HTTPS_IN_EMAIL_LINK = getattr(
|
HELPDESK_USE_HTTPS_IN_EMAIL_LINK = getattr(
|
||||||
settings, 'HELPDESK_USE_HTTPS_IN_EMAIL_LINK', settings.SECURE_SSL_REDIRECT)
|
settings, "HELPDESK_USE_HTTPS_IN_EMAIL_LINK", settings.SECURE_SSL_REDIRECT
|
||||||
|
)
|
||||||
|
|
||||||
# Default to True for backwards compatibility
|
# Default to True for backwards compatibility
|
||||||
HELPDESK_TEAMS_MODE_ENABLED = getattr(settings, 'HELPDESK_TEAMS_MODE_ENABLED', True)
|
HELPDESK_TEAMS_MODE_ENABLED = getattr(settings, "HELPDESK_TEAMS_MODE_ENABLED", True)
|
||||||
if HELPDESK_TEAMS_MODE_ENABLED:
|
if HELPDESK_TEAMS_MODE_ENABLED:
|
||||||
HELPDESK_TEAMS_MODEL = getattr(
|
HELPDESK_TEAMS_MODEL = getattr(settings, "HELPDESK_TEAMS_MODEL", "pinax_teams.Team")
|
||||||
settings, 'HELPDESK_TEAMS_MODEL', 'pinax_teams.Team')
|
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = getattr(
|
||||||
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = getattr(settings, 'HELPDESK_TEAMS_MIGRATION_DEPENDENCIES', [
|
settings,
|
||||||
('pinax_teams', '0004_auto_20170511_0856')])
|
"HELPDESK_TEAMS_MIGRATION_DEPENDENCIES",
|
||||||
|
[("pinax_teams", "0004_auto_20170511_0856")],
|
||||||
|
)
|
||||||
HELPDESK_KBITEM_TEAM_GETTER = getattr(
|
HELPDESK_KBITEM_TEAM_GETTER = getattr(
|
||||||
settings, 'HELPDESK_KBITEM_TEAM_GETTER', lambda kbitem: kbitem.team)
|
settings, "HELPDESK_KBITEM_TEAM_GETTER", lambda kbitem: kbitem.team
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
HELPDESK_TEAMS_MODEL = settings.AUTH_USER_MODEL
|
HELPDESK_TEAMS_MODEL = settings.AUTH_USER_MODEL
|
||||||
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = []
|
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = []
|
||||||
@ -343,35 +394,38 @@ else:
|
|||||||
|
|
||||||
# show knowledgebase links?
|
# show knowledgebase links?
|
||||||
# If Teams mode is enabled then it has to be on
|
# If Teams mode is enabled then it has to be on
|
||||||
HELPDESK_KB_ENABLED = True if HELPDESK_TEAMS_MODE_ENABLED else getattr(settings, 'HELPDESK_KB_ENABLED', True)
|
HELPDESK_KB_ENABLED = (
|
||||||
|
True
|
||||||
|
if HELPDESK_TEAMS_MODE_ENABLED
|
||||||
|
else getattr(settings, "HELPDESK_KB_ENABLED", True)
|
||||||
|
)
|
||||||
|
|
||||||
# Include all signatures and forwards in the first ticket message if set
|
# Include all signatures and forwards in the first ticket message if set
|
||||||
# Useful if you get forwards dropped from them while they are useful part
|
# Useful if you get forwards dropped from them while they are useful part
|
||||||
# of request
|
# of request
|
||||||
HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr(
|
HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr(
|
||||||
settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False)
|
settings, "HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL", False
|
||||||
|
)
|
||||||
|
|
||||||
# If set then we always save incoming emails as .eml attachments
|
# If set then we always save incoming emails as .eml attachments
|
||||||
# which is quite noisy but very helpful for complicated markup, forwards and so on
|
# which is quite noisy but very helpful for complicated markup, forwards and so on
|
||||||
# (which gets stripped/corrupted otherwise)
|
# (which gets stripped/corrupted otherwise)
|
||||||
HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr(
|
HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr(
|
||||||
settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False)
|
settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False
|
||||||
|
)
|
||||||
|
|
||||||
#######################
|
#######################
|
||||||
# email OAUTH #
|
# email OAUTH #
|
||||||
#######################
|
#######################
|
||||||
|
|
||||||
HELPDESK_OAUTH = getattr(
|
HELPDESK_OAUTH = getattr(
|
||||||
settings, 'HELPDESK_OAUTH', {
|
settings,
|
||||||
"token_url": "",
|
"HELPDESK_OAUTH",
|
||||||
"client_id": "",
|
{"token_url": "", "client_id": "", "secret": "", "scope": [""]},
|
||||||
"secret": "",
|
|
||||||
"scope": [""]
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set Debug Logging Level for IMAP Services. Default to '0' for No Debugging
|
# Set Debug Logging Level for IMAP Services. Default to '0' for No Debugging
|
||||||
HELPDESK_IMAP_DEBUG_LEVEL = getattr(settings, 'HELPDESK_IMAP_DEBUG_LEVEL', 0)
|
HELPDESK_IMAP_DEBUG_LEVEL = getattr(settings, "HELPDESK_IMAP_DEBUG_LEVEL", 0)
|
||||||
|
|
||||||
#############################################
|
#############################################
|
||||||
# file permissions - Attachment directories #
|
# file permissions - Attachment directories #
|
||||||
@ -379,29 +433,60 @@ HELPDESK_IMAP_DEBUG_LEVEL = getattr(settings, 'HELPDESK_IMAP_DEBUG_LEVEL', 0)
|
|||||||
|
|
||||||
# Attachment directories should be created with permission 755 (rwxr-xr-x)
|
# Attachment directories should be created with permission 755 (rwxr-xr-x)
|
||||||
# Override it in your own Django settings.py
|
# Override it in your own Django settings.py
|
||||||
HELPDESK_ATTACHMENT_DIR_PERMS = int(getattr(settings, 'HELPDESK_ATTACHMENT_DIR_PERMS', "755"), 8)
|
HELPDESK_ATTACHMENT_DIR_PERMS = int(
|
||||||
|
getattr(settings, "HELPDESK_ATTACHMENT_DIR_PERMS", "755"), 8
|
||||||
|
)
|
||||||
|
|
||||||
HELPDESK_VALID_EXTENSIONS = getattr(settings, 'VALID_EXTENSIONS', None)
|
HELPDESK_VALID_EXTENSIONS = getattr(settings, "VALID_EXTENSIONS", None)
|
||||||
if HELPDESK_VALID_EXTENSIONS:
|
if HELPDESK_VALID_EXTENSIONS:
|
||||||
# Print to stderr
|
# Print to stderr
|
||||||
print("VALID_EXTENSIONS is deprecated, use HELPDESK_VALID_EXTENSIONS instead", file=sys.stderr)
|
print(
|
||||||
|
"VALID_EXTENSIONS is deprecated, use HELPDESK_VALID_EXTENSIONS instead",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
HELPDESK_VALID_EXTENSIONS = getattr(settings, 'HELPDESK_VALID_EXTENSIONS', ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml'])
|
HELPDESK_VALID_EXTENSIONS = getattr(
|
||||||
|
settings,
|
||||||
|
"HELPDESK_VALID_EXTENSIONS",
|
||||||
|
[
|
||||||
|
".txt",
|
||||||
|
".asc",
|
||||||
|
".htm",
|
||||||
|
".html",
|
||||||
|
".pdf",
|
||||||
|
".doc",
|
||||||
|
".docx",
|
||||||
|
".odt",
|
||||||
|
".jpg",
|
||||||
|
".png",
|
||||||
|
".eml",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
HELPDESK_VALIDATE_ATTACHMENT_TYPES = getattr(
|
||||||
|
settings, "HELPDESK_VALIDATE_ATTACHMENT_TYPES", True
|
||||||
|
)
|
||||||
|
|
||||||
HELPDESK_VALIDATE_ATTACHMENT_TYPES = getattr(settings, 'HELPDESK_VALIDATE_ATTACHMENT_TYPES', True)
|
|
||||||
|
|
||||||
def get_followup_webhook_urls():
|
def get_followup_webhook_urls():
|
||||||
urls = os.environ.get('HELPDESK_FOLLOWUP_WEBHOOK_URLS', None)
|
urls = os.environ.get("HELPDESK_FOLLOWUP_WEBHOOK_URLS", None)
|
||||||
if urls:
|
if urls:
|
||||||
return re.split(r'[\s],[\s]', urls)
|
return re.split(r"[\s],[\s]", urls)
|
||||||
|
|
||||||
|
|
||||||
|
HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS = getattr(
|
||||||
|
settings, "HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS", get_followup_webhook_urls
|
||||||
|
)
|
||||||
|
|
||||||
HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS = getattr(settings, 'HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS', get_followup_webhook_urls)
|
|
||||||
|
|
||||||
def get_new_ticket_webhook_urls():
|
def get_new_ticket_webhook_urls():
|
||||||
urls = os.environ.get('HELPDESK_NEW_TICKET_WEBHOOK_URLS', None)
|
urls = os.environ.get("HELPDESK_NEW_TICKET_WEBHOOK_URLS", None)
|
||||||
if urls:
|
if urls:
|
||||||
return urls.split(',')
|
return urls.split(",")
|
||||||
|
|
||||||
HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS = getattr(settings, 'HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS', get_new_ticket_webhook_urls)
|
|
||||||
|
|
||||||
HELPDESK_WEBHOOK_TIMEOUT = getattr(settings, 'HELPDESK_WEBHOOK_TIMEOUT', 3)
|
HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS = getattr(
|
||||||
|
settings, "HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS", get_new_ticket_webhook_urls
|
||||||
|
)
|
||||||
|
|
||||||
|
HELPDESK_WEBHOOK_TIMEOUT = getattr(settings, "HELPDESK_WEBHOOK_TIMEOUT", 3)
|
||||||
|
@ -4,4 +4,4 @@ import django.dispatch
|
|||||||
new_ticket_done = django.dispatch.Signal()
|
new_ticket_done = django.dispatch.Signal()
|
||||||
|
|
||||||
# create a signal for ticket_update view
|
# create a signal for ticket_update view
|
||||||
update_ticket_done = django.dispatch.Signal()
|
update_ticket_done = django.dispatch.Signal()
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
import logging
|
import logging
|
||||||
@ -6,17 +5,19 @@ import os
|
|||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('helpdesk')
|
logger = logging.getLogger("helpdesk")
|
||||||
|
|
||||||
|
|
||||||
def send_templated_mail(template_name,
|
def send_templated_mail(
|
||||||
context,
|
template_name,
|
||||||
recipients,
|
context,
|
||||||
sender=None,
|
recipients,
|
||||||
bcc=None,
|
sender=None,
|
||||||
fail_silently=False,
|
bcc=None,
|
||||||
files=None,
|
fail_silently=False,
|
||||||
extra_headers=None):
|
files=None,
|
||||||
|
extra_headers=None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
send_templated_mail() is a wrapper around Django's e-mail routines that
|
send_templated_mail() is a wrapper around Django's e-mail routines that
|
||||||
allows us to easily send multipart (text/plain & text/html) e-mails using
|
allows us to easily send multipart (text/plain & text/html) e-mails using
|
||||||
@ -48,77 +49,87 @@ def send_templated_mail(template_name,
|
|||||||
"""
|
"""
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
from django.template import engines
|
from django.template import engines
|
||||||
from_string = engines['django'].from_string
|
|
||||||
|
from_string = engines["django"].from_string
|
||||||
|
|
||||||
from helpdesk.models import EmailTemplate
|
from helpdesk.models import EmailTemplate
|
||||||
from helpdesk.settings import HELPDESK_EMAIL_FALLBACK_LOCALE, HELPDESK_EMAIL_SUBJECT_TEMPLATE
|
from helpdesk.settings import (
|
||||||
|
HELPDESK_EMAIL_FALLBACK_LOCALE,
|
||||||
|
HELPDESK_EMAIL_SUBJECT_TEMPLATE,
|
||||||
|
)
|
||||||
|
|
||||||
headers = extra_headers or {}
|
headers = extra_headers or {}
|
||||||
|
|
||||||
locale = context['queue'].get('locale') or HELPDESK_EMAIL_FALLBACK_LOCALE
|
locale = context["queue"].get("locale") or HELPDESK_EMAIL_FALLBACK_LOCALE
|
||||||
|
|
||||||
try:
|
try:
|
||||||
t = EmailTemplate.objects.get(
|
t = EmailTemplate.objects.get(
|
||||||
template_name__iexact=template_name, locale=locale)
|
template_name__iexact=template_name, locale=locale
|
||||||
|
)
|
||||||
except EmailTemplate.DoesNotExist:
|
except EmailTemplate.DoesNotExist:
|
||||||
try:
|
try:
|
||||||
t = EmailTemplate.objects.get(
|
t = EmailTemplate.objects.get(
|
||||||
template_name__iexact=template_name, locale__isnull=True)
|
template_name__iexact=template_name, locale__isnull=True
|
||||||
|
)
|
||||||
except EmailTemplate.DoesNotExist:
|
except EmailTemplate.DoesNotExist:
|
||||||
logger.warning(
|
logger.warning('template "%s" does not exist, no mail sent', template_name)
|
||||||
'template "%s" does not exist, no mail sent', template_name)
|
|
||||||
return # just ignore if template doesn't exist
|
return # just ignore if template doesn't exist
|
||||||
|
|
||||||
subject_part = from_string(
|
subject_part = (
|
||||||
HELPDESK_EMAIL_SUBJECT_TEMPLATE % {
|
from_string(HELPDESK_EMAIL_SUBJECT_TEMPLATE % {"subject": t.subject})
|
||||||
"subject": t.subject
|
.render(context)
|
||||||
}).render(context).replace('\n', '').replace('\r', '')
|
.replace("\n", "")
|
||||||
|
.replace("\r", "")
|
||||||
|
)
|
||||||
|
|
||||||
footer_file = os.path.join('helpdesk', locale, 'email_text_footer.txt')
|
footer_file = os.path.join("helpdesk", locale, "email_text_footer.txt")
|
||||||
|
|
||||||
text_part = from_string(
|
text_part = from_string(
|
||||||
"%s\n\n{%% include '%s' %%}" % (t.plain_text, footer_file)
|
"%s\n\n{%% include '%s' %%}" % (t.plain_text, footer_file)
|
||||||
).render(context)
|
).render(context)
|
||||||
|
|
||||||
email_html_base_file = os.path.join(
|
email_html_base_file = os.path.join("helpdesk", locale, "email_html_base.html")
|
||||||
'helpdesk', locale, 'email_html_base.html')
|
|
||||||
# keep new lines in html emails
|
# keep new lines in html emails
|
||||||
if 'comment' in context:
|
if "comment" in context:
|
||||||
context['comment'] = mark_safe(
|
context["comment"] = mark_safe(context["comment"].replace("\r\n", "<br>"))
|
||||||
context['comment'].replace('\r\n', '<br>'))
|
|
||||||
|
|
||||||
html_part = from_string(
|
html_part = from_string(
|
||||||
"{%% extends '%s' %%}"
|
"{%% extends '%s' %%}"
|
||||||
"{%% block title %%}%s{%% endblock %%}"
|
"{%% block title %%}%s{%% endblock %%}"
|
||||||
"{%% block content %%}%s{%% endblock %%}" %
|
"{%% block content %%}%s{%% endblock %%}"
|
||||||
(email_html_base_file, t.heading, t.html)
|
% (email_html_base_file, t.heading, t.html)
|
||||||
).render(context)
|
).render(context)
|
||||||
|
|
||||||
if isinstance(recipients, str):
|
if isinstance(recipients, str):
|
||||||
if recipients.find(','):
|
if recipients.find(","):
|
||||||
recipients = recipients.split(',')
|
recipients = recipients.split(",")
|
||||||
elif type(recipients) is not list:
|
elif type(recipients) is not list:
|
||||||
recipients = [recipients]
|
recipients = [recipients]
|
||||||
|
|
||||||
msg = EmailMultiAlternatives(subject_part, text_part,
|
msg = EmailMultiAlternatives(
|
||||||
sender or settings.DEFAULT_FROM_EMAIL,
|
subject_part,
|
||||||
recipients, bcc=bcc,
|
text_part,
|
||||||
headers=headers)
|
sender or settings.DEFAULT_FROM_EMAIL,
|
||||||
|
recipients,
|
||||||
|
bcc=bcc,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
msg.attach_alternative(html_part, "text/html")
|
msg.attach_alternative(html_part, "text/html")
|
||||||
|
|
||||||
if files:
|
if files:
|
||||||
for filename, filefield in files:
|
for filename, filefield in files:
|
||||||
filefield.open('rb')
|
filefield.open("rb")
|
||||||
content = filefield.read()
|
content = filefield.read()
|
||||||
msg.attach(filename, content)
|
msg.attach(filename, content)
|
||||||
filefield.close()
|
filefield.close()
|
||||||
logger.debug('Sending email to: {!r}'.format(recipients))
|
logger.debug("Sending email to: {!r}".format(recipients))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return msg.send()
|
return msg.send()
|
||||||
except SMTPException as e:
|
except SMTPException as e:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
'SMTPException raised while sending email to {}'.format(recipients))
|
"SMTPException raised while sending email to {}".format(recipients)
|
||||||
|
)
|
||||||
if not fail_silently:
|
if not fail_silently:
|
||||||
raise e
|
raise e
|
||||||
return 0
|
return 0
|
||||||
|
@ -14,10 +14,9 @@ logger = logging.getLogger(__name__)
|
|||||||
register = Library()
|
register = Library()
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name='is_helpdesk_staff')
|
@register.filter(name="is_helpdesk_staff")
|
||||||
def helpdesk_staff(user):
|
def helpdesk_staff(user):
|
||||||
try:
|
try:
|
||||||
return is_helpdesk_staff(user)
|
return is_helpdesk_staff(user)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(
|
logger.exception("'helpdesk_staff' template tag (django-helpdesk) crashed")
|
||||||
"'helpdesk_staff' template tag (django-helpdesk) crashed")
|
|
||||||
|
@ -2,7 +2,11 @@ from datetime import datetime
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template import Library
|
from django.template import Library
|
||||||
from django.template.defaultfilters import date as date_filter
|
from django.template.defaultfilters import date as date_filter
|
||||||
from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT
|
from helpdesk.forms import (
|
||||||
|
CUSTOMFIELD_DATE_FORMAT,
|
||||||
|
CUSTOMFIELD_DATETIME_FORMAT,
|
||||||
|
CUSTOMFIELD_TIME_FORMAT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
register = Library()
|
register = Library()
|
||||||
@ -10,7 +14,7 @@ register = Library()
|
|||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def get(value, arg, default=None):
|
def get(value, arg, default=None):
|
||||||
""" Call the dictionary get function """
|
"""Call the dictionary get function"""
|
||||||
return value.get(arg, default)
|
return value.get(arg, default)
|
||||||
|
|
||||||
|
|
||||||
@ -21,16 +25,21 @@ def datetime_string_format(value):
|
|||||||
:return: String - reformatted to default datetime, date, or time string if received in one of the expected formats
|
:return: String - reformatted to default datetime, date, or time string if received in one of the expected formats
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
new_value = date_filter(datetime.strptime(
|
new_value = date_filter(
|
||||||
value, CUSTOMFIELD_DATETIME_FORMAT), settings.DATETIME_FORMAT)
|
datetime.strptime(value, CUSTOMFIELD_DATETIME_FORMAT),
|
||||||
|
settings.DATETIME_FORMAT,
|
||||||
|
)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
try:
|
try:
|
||||||
new_value = date_filter(datetime.strptime(
|
new_value = date_filter(
|
||||||
value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT)
|
datetime.strptime(value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT
|
||||||
|
)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
try:
|
try:
|
||||||
new_value = date_filter(datetime.strptime(
|
new_value = date_filter(
|
||||||
value, CUSTOMFIELD_TIME_FORMAT), settings.TIME_FORMAT)
|
datetime.strptime(value, CUSTOMFIELD_TIME_FORMAT),
|
||||||
|
settings.TIME_FORMAT,
|
||||||
|
)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
# If NoneType return empty string, else return original value
|
# If NoneType return empty string, else return original value
|
||||||
new_value = "" if value is None else value
|
new_value = "" if value is None else value
|
||||||
|
@ -4,6 +4,7 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
|
|||||||
templatetags/load_helpdesk_settings.py - returns the settings as defined in
|
templatetags/load_helpdesk_settings.py - returns the settings as defined in
|
||||||
django-helpdesk/helpdesk/settings.py
|
django-helpdesk/helpdesk/settings.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.template import Library
|
from django.template import Library
|
||||||
from helpdesk import settings as helpdesk_settings_config
|
from helpdesk import settings as helpdesk_settings_config
|
||||||
|
|
||||||
@ -13,11 +14,14 @@ def load_helpdesk_settings(request):
|
|||||||
return helpdesk_settings_config
|
return helpdesk_settings_config
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import sys
|
import sys
|
||||||
print("'load_helpdesk_settings' template tag (django-helpdesk) crashed with following error:",
|
|
||||||
file=sys.stderr)
|
print(
|
||||||
|
"'load_helpdesk_settings' template tag (django-helpdesk) crashed with following error:",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
print(e, file=sys.stderr)
|
print(e, file=sys.stderr)
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
|
|
||||||
register = Library()
|
register = Library()
|
||||||
register.filter('load_helpdesk_settings', load_helpdesk_settings)
|
register.filter("load_helpdesk_settings", load_helpdesk_settings)
|
||||||
|
@ -5,6 +5,7 @@ templatetags/saved_queries.py - This template tag returns previously saved
|
|||||||
queries. Therefore you don't need to modify
|
queries. Therefore you don't need to modify
|
||||||
any views.
|
any views.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from helpdesk.models import SavedSearch
|
from helpdesk.models import SavedSearch
|
||||||
@ -23,7 +24,10 @@ def saved_queries(user):
|
|||||||
return user_saved_queries
|
return user_saved_queries
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import sys
|
import sys
|
||||||
print("'saved_queries' template tag (django-helpdesk) crashed with following error:",
|
|
||||||
file=sys.stderr)
|
print(
|
||||||
|
"'saved_queries' template tag (django-helpdesk) crashed with following error:",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
print(e, file=sys.stderr)
|
print(e, file=sys.stderr)
|
||||||
return ''
|
return ""
|
||||||
|
@ -19,7 +19,7 @@ import re
|
|||||||
|
|
||||||
|
|
||||||
def num_to_link(text):
|
def num_to_link(text):
|
||||||
if text == '':
|
if text == "":
|
||||||
return text
|
return text
|
||||||
|
|
||||||
matches = []
|
matches = []
|
||||||
@ -28,7 +28,7 @@ def num_to_link(text):
|
|||||||
|
|
||||||
for match in reversed(matches):
|
for match in reversed(matches):
|
||||||
number = match.groups()[0]
|
number = match.groups()[0]
|
||||||
url = reverse('helpdesk:view', args=[number])
|
url = reverse("helpdesk:view", args=[number])
|
||||||
try:
|
try:
|
||||||
ticket = Ticket.objects.get(id=number)
|
ticket = Ticket.objects.get(id=number)
|
||||||
except Ticket.DoesNotExist:
|
except Ticket.DoesNotExist:
|
||||||
@ -36,8 +36,16 @@ def num_to_link(text):
|
|||||||
|
|
||||||
if ticket:
|
if ticket:
|
||||||
style = ticket.get_status_display()
|
style = ticket.get_status_display()
|
||||||
text = "%s <a href='%s' class='ticket_link_status ticket_link_status_%s'>#%s</a>%s" % (
|
text = (
|
||||||
text[:match.start() + 1], url, style, match.groups()[0], text[match.end():])
|
"%s <a href='%s' class='ticket_link_status ticket_link_status_%s'>#%s</a>%s"
|
||||||
|
% (
|
||||||
|
text[: match.start() + 1],
|
||||||
|
url,
|
||||||
|
style,
|
||||||
|
match.groups()[0],
|
||||||
|
text[match.end() :],
|
||||||
|
)
|
||||||
|
)
|
||||||
return mark_safe(text)
|
return mark_safe(text)
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,9 +20,7 @@ def user_admin_url(action):
|
|||||||
except AttributeError: # module_name alias removed in django 1.8
|
except AttributeError: # module_name alias removed in django 1.8
|
||||||
model_name = user._meta.model_name.lower()
|
model_name = user._meta.model_name.lower()
|
||||||
|
|
||||||
return 'admin:%s_%s_%s' % (
|
return "admin:%s_%s_%s" % (user._meta.app_label, model_name, action)
|
||||||
user._meta.app_label, model_name,
|
|
||||||
action)
|
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
@ -8,16 +8,15 @@ import sys
|
|||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
def get_user(username='helpdesk.staff',
|
def get_user(
|
||||||
password='password',
|
username="helpdesk.staff", password="password", is_staff=False, is_superuser=False
|
||||||
is_staff=False,
|
):
|
||||||
is_superuser=False):
|
|
||||||
try:
|
try:
|
||||||
user = User.objects.get(username=username)
|
user = User.objects.get(username=username)
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
user = User.objects.create_user(username=username,
|
user = User.objects.create_user(
|
||||||
password=password,
|
username=username, password=password, email="%s@example.com" % username
|
||||||
email='%s@example.com' % username)
|
)
|
||||||
user.is_staff = is_staff
|
user.is_staff = is_staff
|
||||||
user.is_superuser = is_superuser
|
user.is_superuser = is_superuser
|
||||||
user.save()
|
user.save()
|
||||||
@ -32,7 +31,6 @@ def get_staff_user():
|
|||||||
|
|
||||||
|
|
||||||
def reload_urlconf(urlconf=None):
|
def reload_urlconf(urlconf=None):
|
||||||
|
|
||||||
from importlib import reload
|
from importlib import reload
|
||||||
|
|
||||||
if urlconf is None:
|
if urlconf is None:
|
||||||
@ -47,25 +45,29 @@ def reload_urlconf(urlconf=None):
|
|||||||
reload(sys.modules[urlconf])
|
reload(sys.modules[urlconf])
|
||||||
|
|
||||||
from django.urls import clear_url_caches
|
from django.urls import clear_url_caches
|
||||||
|
|
||||||
clear_url_caches()
|
clear_url_caches()
|
||||||
|
|
||||||
|
|
||||||
def create_ticket(**kwargs):
|
def create_ticket(**kwargs):
|
||||||
q = kwargs.get('queue', None)
|
q = kwargs.get("queue", None)
|
||||||
if q is None:
|
if q is None:
|
||||||
try:
|
try:
|
||||||
q = Queue.objects.all()[0]
|
q = Queue.objects.all()[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
q = Queue.objects.create(title='Test Q', slug='test', )
|
q = Queue.objects.create(
|
||||||
|
title="Test Q",
|
||||||
|
slug="test",
|
||||||
|
)
|
||||||
data = {
|
data = {
|
||||||
'title': "I wish to register a complaint",
|
"title": "I wish to register a complaint",
|
||||||
'queue': q,
|
"queue": q,
|
||||||
}
|
}
|
||||||
data.update(kwargs)
|
data.update(kwargs)
|
||||||
return Ticket.objects.create(**data)
|
return Ticket.objects.create(**data)
|
||||||
|
|
||||||
|
|
||||||
HELPDESK_URLCONF = 'helpdesk.urls'
|
HELPDESK_URLCONF = "helpdesk.urls"
|
||||||
|
|
||||||
|
|
||||||
def print_response(response, stdout=False):
|
def print_response(response, stdout=False):
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -13,7 +12,7 @@ from rest_framework.status import (
|
|||||||
HTTP_201_CREATED,
|
HTTP_201_CREATED,
|
||||||
HTTP_204_NO_CONTENT,
|
HTTP_204_NO_CONTENT,
|
||||||
HTTP_400_BAD_REQUEST,
|
HTTP_400_BAD_REQUEST,
|
||||||
HTTP_403_FORBIDDEN
|
HTTP_403_FORBIDDEN,
|
||||||
)
|
)
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
@ -24,106 +23,124 @@ class TicketTest(APITestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.queue = Queue.objects.create(
|
cls.queue = Queue.objects.create(
|
||||||
title='Test Queue',
|
title="Test Queue",
|
||||||
slug='test-queue',
|
slug="test-queue",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_api_ticket_not_authenticated_user(self):
|
def test_create_api_ticket_not_authenticated_user(self):
|
||||||
response = self.client.post('/api/tickets/')
|
response = self.client.post("/api/tickets/")
|
||||||
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
def test_create_api_ticket_authenticated_non_staff_user(self):
|
def test_create_api_ticket_authenticated_non_staff_user(self):
|
||||||
non_staff_user = User.objects.create_user(username='test')
|
non_staff_user = User.objects.create_user(username="test")
|
||||||
self.client.force_authenticate(non_staff_user)
|
self.client.force_authenticate(non_staff_user)
|
||||||
response = self.client.post('/api/tickets/')
|
response = self.client.post("/api/tickets/")
|
||||||
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
def test_create_api_ticket_no_data(self):
|
def test_create_api_ticket_no_data(self):
|
||||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
staff_user = User.objects.create_user(username="test", is_staff=True)
|
||||||
self.client.force_authenticate(staff_user)
|
self.client.force_authenticate(staff_user)
|
||||||
response = self.client.post('/api/tickets/')
|
response = self.client.post("/api/tickets/")
|
||||||
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(response.data, {
|
self.assertEqual(
|
||||||
'queue': [ErrorDetail(string='This field is required.', code='required')],
|
response.data,
|
||||||
'title': [ErrorDetail(string='This field is required.', code='required')]
|
{
|
||||||
})
|
"queue": [
|
||||||
|
ErrorDetail(string="This field is required.", code="required")
|
||||||
|
],
|
||||||
|
"title": [
|
||||||
|
ErrorDetail(string="This field is required.", code="required")
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertFalse(Ticket.objects.exists())
|
self.assertFalse(Ticket.objects.exists())
|
||||||
|
|
||||||
def test_create_api_ticket_wrong_date_format(self):
|
def test_create_api_ticket_wrong_date_format(self):
|
||||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
staff_user = User.objects.create_user(username="test", is_staff=True)
|
||||||
self.client.force_authenticate(staff_user)
|
self.client.force_authenticate(staff_user)
|
||||||
response = self.client.post('/api/tickets/', {
|
response = self.client.post(
|
||||||
'queue': self.queue.id,
|
"/api/tickets/",
|
||||||
'title': 'Test title',
|
{
|
||||||
'due_date': 'monday, 1st of may 2022'
|
"queue": self.queue.id,
|
||||||
})
|
"title": "Test title",
|
||||||
|
"due_date": "monday, 1st of may 2022",
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(response.data, {
|
self.assertEqual(
|
||||||
'due_date': [ErrorDetail(string='Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].', code='invalid')]
|
response.data,
|
||||||
})
|
{
|
||||||
|
"due_date": [
|
||||||
|
ErrorDetail(
|
||||||
|
string="Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].",
|
||||||
|
code="invalid",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertFalse(Ticket.objects.exists())
|
self.assertFalse(Ticket.objects.exists())
|
||||||
|
|
||||||
def test_create_api_ticket_authenticated_staff_user(self):
|
def test_create_api_ticket_authenticated_staff_user(self):
|
||||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
staff_user = User.objects.create_user(username="test", is_staff=True)
|
||||||
self.client.force_authenticate(staff_user)
|
self.client.force_authenticate(staff_user)
|
||||||
response = self.client.post('/api/tickets/', {
|
response = self.client.post(
|
||||||
'queue': self.queue.id,
|
"/api/tickets/",
|
||||||
'title': 'Test title',
|
{
|
||||||
'description': 'Test description\nMulti lines',
|
"queue": self.queue.id,
|
||||||
'submitter_email': 'test@mail.com',
|
"title": "Test title",
|
||||||
'priority': 4
|
"description": "Test description\nMulti lines",
|
||||||
})
|
"submitter_email": "test@mail.com",
|
||||||
|
"priority": 4,
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||||
created_ticket = Ticket.objects.get()
|
created_ticket = Ticket.objects.get()
|
||||||
self.assertEqual(created_ticket.title, 'Test title')
|
self.assertEqual(created_ticket.title, "Test title")
|
||||||
self.assertEqual(created_ticket.description,
|
self.assertEqual(created_ticket.description, "Test description\nMulti lines")
|
||||||
'Test description\nMulti lines')
|
self.assertEqual(created_ticket.submitter_email, "test@mail.com")
|
||||||
self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
|
|
||||||
self.assertEqual(created_ticket.priority, 4)
|
self.assertEqual(created_ticket.priority, 4)
|
||||||
self.assertEqual(created_ticket.followup_set.count(), 1)
|
self.assertEqual(created_ticket.followup_set.count(), 1)
|
||||||
|
|
||||||
def test_create_api_ticket_with_basic_auth(self):
|
def test_create_api_ticket_with_basic_auth(self):
|
||||||
username = 'admin'
|
username = "admin"
|
||||||
password = 'admin'
|
password = "admin"
|
||||||
User.objects.create_user(
|
User.objects.create_user(username=username, password=password, is_staff=True)
|
||||||
username=username, password=password, is_staff=True)
|
|
||||||
|
|
||||||
test_user = User.objects.create_user(username='test')
|
test_user = User.objects.create_user(username="test")
|
||||||
merge_ticket = Ticket.objects.create(
|
merge_ticket = Ticket.objects.create(queue=self.queue, title="merge ticket")
|
||||||
queue=self.queue, title='merge ticket')
|
|
||||||
|
|
||||||
# Generate base64 credentials string
|
# Generate base64 credentials string
|
||||||
credentials = f"{username}:{password}"
|
credentials = f"{username}:{password}"
|
||||||
base64_credentials = base64.b64encode(credentials.encode(
|
base64_credentials = base64.b64encode(
|
||||||
HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
|
credentials.encode(HTTP_HEADER_ENCODING)
|
||||||
|
).decode(HTTP_HEADER_ENCODING)
|
||||||
|
|
||||||
self.client.credentials(
|
self.client.credentials(HTTP_AUTHORIZATION=f"Basic {base64_credentials}")
|
||||||
HTTP_AUTHORIZATION=f"Basic {base64_credentials}")
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
'/api/tickets/',
|
"/api/tickets/",
|
||||||
{
|
{
|
||||||
'queue': self.queue.id,
|
"queue": self.queue.id,
|
||||||
'title': 'Title',
|
"title": "Title",
|
||||||
'description': 'Description',
|
"description": "Description",
|
||||||
'resolution': 'Resolution',
|
"resolution": "Resolution",
|
||||||
'assigned_to': test_user.id,
|
"assigned_to": test_user.id,
|
||||||
'submitter_email': 'test@mail.com',
|
"submitter_email": "test@mail.com",
|
||||||
'status': Ticket.RESOLVED_STATUS,
|
"status": Ticket.RESOLVED_STATUS,
|
||||||
'priority': 1,
|
"priority": 1,
|
||||||
'on_hold': True,
|
"on_hold": True,
|
||||||
'due_date': self.due_date,
|
"due_date": self.due_date,
|
||||||
'merged_to': merge_ticket.id
|
"merged_to": merge_ticket.id,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||||
created_ticket = Ticket.objects.last()
|
created_ticket = Ticket.objects.last()
|
||||||
self.assertEqual(created_ticket.title, 'Title')
|
self.assertEqual(created_ticket.title, "Title")
|
||||||
self.assertEqual(created_ticket.description, 'Description')
|
self.assertEqual(created_ticket.description, "Description")
|
||||||
# resolution can not be set on creation
|
# resolution can not be set on creation
|
||||||
self.assertIsNone(created_ticket.resolution)
|
self.assertIsNone(created_ticket.resolution)
|
||||||
self.assertEqual(created_ticket.assigned_to, test_user)
|
self.assertEqual(created_ticket.assigned_to, test_user)
|
||||||
self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
|
self.assertEqual(created_ticket.submitter_email, "test@mail.com")
|
||||||
self.assertEqual(created_ticket.priority, 1)
|
self.assertEqual(created_ticket.priority, 1)
|
||||||
# on_hold is False on creation
|
# on_hold is False on creation
|
||||||
self.assertFalse(created_ticket.on_hold)
|
self.assertFalse(created_ticket.on_hold)
|
||||||
@ -134,39 +151,37 @@ class TicketTest(APITestCase):
|
|||||||
self.assertIsNone(created_ticket.merged_to)
|
self.assertIsNone(created_ticket.merged_to)
|
||||||
|
|
||||||
def test_edit_api_ticket(self):
|
def test_edit_api_ticket(self):
|
||||||
staff_user = User.objects.create_user(username='admin', is_staff=True)
|
staff_user = User.objects.create_user(username="admin", is_staff=True)
|
||||||
test_ticket = Ticket.objects.create(
|
test_ticket = Ticket.objects.create(queue=self.queue, title="Test ticket")
|
||||||
queue=self.queue, title='Test ticket')
|
|
||||||
|
|
||||||
test_user = User.objects.create_user(username='test')
|
test_user = User.objects.create_user(username="test")
|
||||||
merge_ticket = Ticket.objects.create(
|
merge_ticket = Ticket.objects.create(queue=self.queue, title="merge ticket")
|
||||||
queue=self.queue, title='merge ticket')
|
|
||||||
|
|
||||||
self.client.force_authenticate(staff_user)
|
self.client.force_authenticate(staff_user)
|
||||||
response = self.client.put(
|
response = self.client.put(
|
||||||
'/api/tickets/%d/' % test_ticket.id,
|
"/api/tickets/%d/" % test_ticket.id,
|
||||||
{
|
{
|
||||||
'queue': self.queue.id,
|
"queue": self.queue.id,
|
||||||
'title': 'Title',
|
"title": "Title",
|
||||||
'description': 'Description',
|
"description": "Description",
|
||||||
'resolution': 'Resolution',
|
"resolution": "Resolution",
|
||||||
'assigned_to': test_user.id,
|
"assigned_to": test_user.id,
|
||||||
'submitter_email': 'test@mail.com',
|
"submitter_email": "test@mail.com",
|
||||||
'status': Ticket.RESOLVED_STATUS,
|
"status": Ticket.RESOLVED_STATUS,
|
||||||
'priority': 1,
|
"priority": 1,
|
||||||
'on_hold': True,
|
"on_hold": True,
|
||||||
'due_date': self.due_date,
|
"due_date": self.due_date,
|
||||||
'merged_to': merge_ticket.id
|
"merged_to": merge_ticket.id,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, HTTP_200_OK)
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
test_ticket.refresh_from_db()
|
test_ticket.refresh_from_db()
|
||||||
self.assertEqual(test_ticket.title, 'Title')
|
self.assertEqual(test_ticket.title, "Title")
|
||||||
self.assertEqual(test_ticket.description, 'Description')
|
self.assertEqual(test_ticket.description, "Description")
|
||||||
self.assertEqual(test_ticket.resolution, 'Resolution')
|
self.assertEqual(test_ticket.resolution, "Resolution")
|
||||||
self.assertEqual(test_ticket.assigned_to, test_user)
|
self.assertEqual(test_ticket.assigned_to, test_user)
|
||||||
self.assertEqual(test_ticket.submitter_email, 'test@mail.com')
|
self.assertEqual(test_ticket.submitter_email, "test@mail.com")
|
||||||
self.assertEqual(test_ticket.priority, 1)
|
self.assertEqual(test_ticket.priority, 1)
|
||||||
self.assertTrue(test_ticket.on_hold)
|
self.assertTrue(test_ticket.on_hold)
|
||||||
self.assertEqual(test_ticket.status, Ticket.RESOLVED_STATUS)
|
self.assertEqual(test_ticket.status, Ticket.RESOLVED_STATUS)
|
||||||
@ -174,236 +189,264 @@ class TicketTest(APITestCase):
|
|||||||
self.assertEqual(test_ticket.merged_to, merge_ticket)
|
self.assertEqual(test_ticket.merged_to, merge_ticket)
|
||||||
|
|
||||||
def test_partial_edit_api_ticket(self):
|
def test_partial_edit_api_ticket(self):
|
||||||
staff_user = User.objects.create_user(username='admin', is_staff=True)
|
staff_user = User.objects.create_user(username="admin", is_staff=True)
|
||||||
test_ticket = Ticket.objects.create(
|
test_ticket = Ticket.objects.create(queue=self.queue, title="Test ticket")
|
||||||
queue=self.queue, title='Test ticket')
|
|
||||||
|
|
||||||
self.client.force_authenticate(staff_user)
|
self.client.force_authenticate(staff_user)
|
||||||
response = self.client.patch(
|
response = self.client.patch(
|
||||||
'/api/tickets/%d/' % test_ticket.id,
|
"/api/tickets/%d/" % test_ticket.id,
|
||||||
{
|
{
|
||||||
'description': 'New description',
|
"description": "New description",
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, HTTP_200_OK)
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
test_ticket.refresh_from_db()
|
test_ticket.refresh_from_db()
|
||||||
self.assertEqual(test_ticket.description, 'New description')
|
self.assertEqual(test_ticket.description, "New description")
|
||||||
|
|
||||||
def test_delete_api_ticket(self):
|
def test_delete_api_ticket(self):
|
||||||
staff_user = User.objects.create_user(username='admin', is_staff=True)
|
staff_user = User.objects.create_user(username="admin", is_staff=True)
|
||||||
test_ticket = Ticket.objects.create(
|
test_ticket = Ticket.objects.create(queue=self.queue, title="Test ticket")
|
||||||
queue=self.queue, title='Test ticket')
|
|
||||||
self.client.force_authenticate(staff_user)
|
self.client.force_authenticate(staff_user)
|
||||||
response = self.client.delete('/api/tickets/%d/' % test_ticket.id)
|
response = self.client.delete("/api/tickets/%d/" % test_ticket.id)
|
||||||
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
|
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
|
||||||
self.assertFalse(Ticket.objects.exists())
|
self.assertFalse(Ticket.objects.exists())
|
||||||
|
|
||||||
@freeze_time('2022-06-30 23:09:44')
|
@freeze_time("2022-06-30 23:09:44")
|
||||||
def test_create_api_ticket_with_custom_fields(self):
|
def test_create_api_ticket_with_custom_fields(self):
|
||||||
# Create custom fields
|
# Create custom fields
|
||||||
for field_type, field_display in CustomField.DATA_TYPE_CHOICES:
|
for field_type, field_display in CustomField.DATA_TYPE_CHOICES:
|
||||||
extra_data = {}
|
extra_data = {}
|
||||||
if field_type in ('varchar', 'text'):
|
if field_type in ("varchar", "text"):
|
||||||
extra_data['max_length'] = 10
|
extra_data["max_length"] = 10
|
||||||
if field_type == 'integer':
|
if field_type == "integer":
|
||||||
# Set one field as required to test error if not provided
|
# Set one field as required to test error if not provided
|
||||||
extra_data['required'] = True
|
extra_data["required"] = True
|
||||||
if field_type == 'decimal':
|
if field_type == "decimal":
|
||||||
extra_data['max_length'] = 7
|
extra_data["max_length"] = 7
|
||||||
extra_data['decimal_places'] = 3
|
extra_data["decimal_places"] = 3
|
||||||
if field_type == 'list':
|
if field_type == "list":
|
||||||
extra_data['list_values'] = '''Green
|
extra_data["list_values"] = """Green
|
||||||
Blue
|
Blue
|
||||||
Red
|
Red
|
||||||
Yellow'''
|
Yellow"""
|
||||||
CustomField.objects.create(
|
CustomField.objects.create(
|
||||||
name=field_type, label=field_display, data_type=field_type, **extra_data)
|
name=field_type, label=field_display, data_type=field_type, **extra_data
|
||||||
|
)
|
||||||
|
|
||||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
staff_user = User.objects.create_user(username="test", is_staff=True)
|
||||||
self.client.force_authenticate(staff_user)
|
self.client.force_authenticate(staff_user)
|
||||||
|
|
||||||
# Test creation without providing required field
|
# Test creation without providing required field
|
||||||
response = self.client.post('/api/tickets/', {
|
response = self.client.post(
|
||||||
'queue': self.queue.id,
|
"/api/tickets/",
|
||||||
'title': 'Test title',
|
{
|
||||||
'description': 'Test description\nMulti lines',
|
"queue": self.queue.id,
|
||||||
'submitter_email': 'test@mail.com',
|
"title": "Test title",
|
||||||
'priority': 4
|
"description": "Test description\nMulti lines",
|
||||||
})
|
"submitter_email": "test@mail.com",
|
||||||
|
"priority": 4,
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(response.data, {'custom_integer': [ErrorDetail(
|
self.assertEqual(
|
||||||
string='This field is required.', code='required')]})
|
response.data,
|
||||||
|
{
|
||||||
|
"custom_integer": [
|
||||||
|
ErrorDetail(string="This field is required.", code="required")
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# Test creation with custom field values
|
# Test creation with custom field values
|
||||||
response = self.client.post('/api/tickets/', {
|
response = self.client.post(
|
||||||
'queue': self.queue.id,
|
"/api/tickets/",
|
||||||
'title': 'Test title',
|
{
|
||||||
'description': 'Test description\nMulti lines',
|
"queue": self.queue.id,
|
||||||
'submitter_email': 'test@mail.com',
|
"title": "Test title",
|
||||||
'priority': 4,
|
"description": "Test description\nMulti lines",
|
||||||
'custom_varchar': 'test',
|
"submitter_email": "test@mail.com",
|
||||||
'custom_text': 'multi\nline',
|
"priority": 4,
|
||||||
'custom_integer': '1',
|
"custom_varchar": "test",
|
||||||
'custom_decimal': '42.987',
|
"custom_text": "multi\nline",
|
||||||
'custom_list': 'Red',
|
"custom_integer": "1",
|
||||||
'custom_boolean': True,
|
"custom_decimal": "42.987",
|
||||||
'custom_date': '2022-4-11',
|
"custom_list": "Red",
|
||||||
'custom_time': '23:59:59',
|
"custom_boolean": True,
|
||||||
'custom_datetime': '2022-4-10 18:27',
|
"custom_date": "2022-4-11",
|
||||||
'custom_email': 'email@test.com',
|
"custom_time": "23:59:59",
|
||||||
'custom_url': 'http://django-helpdesk.readthedocs.org/',
|
"custom_datetime": "2022-4-10 18:27",
|
||||||
'custom_ipaddress': '127.0.0.1',
|
"custom_email": "email@test.com",
|
||||||
'custom_slug': 'test-slug',
|
"custom_url": "http://django-helpdesk.readthedocs.org/",
|
||||||
})
|
"custom_ipaddress": "127.0.0.1",
|
||||||
|
"custom_slug": "test-slug",
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||||
# Check all fields with data returned from the response
|
# Check all fields with data returned from the response
|
||||||
self.assertEqual(response.data, {
|
self.assertEqual(
|
||||||
'id': 1,
|
response.data,
|
||||||
'queue': 1,
|
{
|
||||||
'title': 'Test title',
|
"id": 1,
|
||||||
'description': 'Test description\nMulti lines',
|
"queue": 1,
|
||||||
'resolution': None,
|
"title": "Test title",
|
||||||
'submitter_email': 'test@mail.com',
|
"description": "Test description\nMulti lines",
|
||||||
'assigned_to': None,
|
"resolution": None,
|
||||||
'status': 1,
|
"submitter_email": "test@mail.com",
|
||||||
'on_hold': False,
|
"assigned_to": None,
|
||||||
'priority': 4,
|
"status": 1,
|
||||||
'due_date': None,
|
"on_hold": False,
|
||||||
'merged_to': None,
|
"priority": 4,
|
||||||
'followup_set': [OrderedDict([
|
"due_date": None,
|
||||||
('id', 1),
|
"merged_to": None,
|
||||||
('ticket', 1),
|
"followup_set": [
|
||||||
('user', 1),
|
OrderedDict(
|
||||||
('title', 'Ticket Opened'),
|
[
|
||||||
('comment', 'Test description\nMulti lines'),
|
("id", 1),
|
||||||
('public', True),
|
("ticket", 1),
|
||||||
('new_status', None),
|
("user", 1),
|
||||||
('time_spent', None),
|
("title", "Ticket Opened"),
|
||||||
('followupattachment_set', []),
|
("comment", "Test description\nMulti lines"),
|
||||||
('date', '2022-06-30T23:09:44'),
|
("public", True),
|
||||||
('message_id', None),
|
("new_status", None),
|
||||||
])],
|
("time_spent", None),
|
||||||
'custom_varchar': 'test',
|
("followupattachment_set", []),
|
||||||
'custom_text': 'multi\nline',
|
("date", "2022-06-30T23:09:44"),
|
||||||
'custom_integer': 1,
|
("message_id", None),
|
||||||
'custom_decimal': '42.987',
|
]
|
||||||
'custom_list': 'Red',
|
)
|
||||||
'custom_boolean': True,
|
],
|
||||||
'custom_date': '2022-04-11',
|
"custom_varchar": "test",
|
||||||
'custom_time': '23:59:59',
|
"custom_text": "multi\nline",
|
||||||
'custom_datetime': '2022-04-10T18:27',
|
"custom_integer": 1,
|
||||||
'custom_email': 'email@test.com',
|
"custom_decimal": "42.987",
|
||||||
'custom_url': 'http://django-helpdesk.readthedocs.org/',
|
"custom_list": "Red",
|
||||||
'custom_ipaddress': '127.0.0.1',
|
"custom_boolean": True,
|
||||||
'custom_slug': 'test-slug'
|
"custom_date": "2022-04-11",
|
||||||
})
|
"custom_time": "23:59:59",
|
||||||
|
"custom_datetime": "2022-04-10T18:27",
|
||||||
|
"custom_email": "email@test.com",
|
||||||
|
"custom_url": "http://django-helpdesk.readthedocs.org/",
|
||||||
|
"custom_ipaddress": "127.0.0.1",
|
||||||
|
"custom_slug": "test-slug",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def test_create_api_ticket_with_attachment(self):
|
def test_create_api_ticket_with_attachment(self):
|
||||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
staff_user = User.objects.create_user(username="test", is_staff=True)
|
||||||
self.client.force_authenticate(staff_user)
|
self.client.force_authenticate(staff_user)
|
||||||
test_file = SimpleUploadedFile(
|
test_file = SimpleUploadedFile(
|
||||||
'file.jpg', b'file_content', content_type='image/jpg')
|
"file.jpg", b"file_content", content_type="image/jpg"
|
||||||
response = self.client.post('/api/tickets/', {
|
)
|
||||||
'queue': self.queue.id,
|
response = self.client.post(
|
||||||
'title': 'Test title',
|
"/api/tickets/",
|
||||||
'description': 'Test description\nMulti lines',
|
{
|
||||||
'submitter_email': 'test@mail.com',
|
"queue": self.queue.id,
|
||||||
'priority': 4,
|
"title": "Test title",
|
||||||
'attachment': test_file
|
"description": "Test description\nMulti lines",
|
||||||
})
|
"submitter_email": "test@mail.com",
|
||||||
|
"priority": 4,
|
||||||
|
"attachment": test_file,
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||||
created_ticket = Ticket.objects.get()
|
created_ticket = Ticket.objects.get()
|
||||||
self.assertEqual(created_ticket.title, 'Test title')
|
self.assertEqual(created_ticket.title, "Test title")
|
||||||
self.assertEqual(created_ticket.description,
|
self.assertEqual(created_ticket.description, "Test description\nMulti lines")
|
||||||
'Test description\nMulti lines')
|
self.assertEqual(created_ticket.submitter_email, "test@mail.com")
|
||||||
self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
|
|
||||||
self.assertEqual(created_ticket.priority, 4)
|
self.assertEqual(created_ticket.priority, 4)
|
||||||
self.assertEqual(created_ticket.followup_set.count(), 1)
|
self.assertEqual(created_ticket.followup_set.count(), 1)
|
||||||
self.assertEqual(created_ticket.followup_set.get(
|
self.assertEqual(
|
||||||
).followupattachment_set.count(), 1)
|
created_ticket.followup_set.get().followupattachment_set.count(), 1
|
||||||
|
)
|
||||||
attachment = created_ticket.followup_set.get().followupattachment_set.get()
|
attachment = created_ticket.followup_set.get().followupattachment_set.get()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
attachment.file.name,
|
attachment.file.name,
|
||||||
f'helpdesk/attachments/test-queue-1-{created_ticket.secret_key}/1/file.jpg'
|
f"helpdesk/attachments/test-queue-1-{created_ticket.secret_key}/1/file.jpg",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_follow_up_with_attachments(self):
|
def test_create_follow_up_with_attachments(self):
|
||||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
staff_user = User.objects.create_user(username="test", is_staff=True)
|
||||||
self.client.force_authenticate(staff_user)
|
self.client.force_authenticate(staff_user)
|
||||||
ticket = Ticket.objects.create(queue=self.queue, title='Test')
|
ticket = Ticket.objects.create(queue=self.queue, title="Test")
|
||||||
test_file_1 = SimpleUploadedFile(
|
test_file_1 = SimpleUploadedFile(
|
||||||
'file.jpg', b'file_content', content_type='image/jpg')
|
"file.jpg", b"file_content", content_type="image/jpg"
|
||||||
|
)
|
||||||
test_file_2 = SimpleUploadedFile(
|
test_file_2 = SimpleUploadedFile(
|
||||||
'doc.pdf', b'Doc content', content_type='application/pdf')
|
"doc.pdf", b"Doc content", content_type="application/pdf"
|
||||||
|
)
|
||||||
|
|
||||||
response = self.client.post('/api/followups/', {
|
response = self.client.post(
|
||||||
'ticket': ticket.id,
|
"/api/followups/",
|
||||||
'title': 'Test',
|
{
|
||||||
'comment': 'Test answer\nMulti lines',
|
"ticket": ticket.id,
|
||||||
'attachments': [
|
"title": "Test",
|
||||||
test_file_1,
|
"comment": "Test answer\nMulti lines",
|
||||||
test_file_2
|
"attachments": [test_file_1, test_file_2],
|
||||||
]
|
},
|
||||||
})
|
)
|
||||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||||
created_followup = ticket.followup_set.last()
|
created_followup = ticket.followup_set.last()
|
||||||
self.assertEqual(created_followup.title, 'Test')
|
self.assertEqual(created_followup.title, "Test")
|
||||||
self.assertEqual(created_followup.comment, 'Test answer\nMulti lines')
|
self.assertEqual(created_followup.comment, "Test answer\nMulti lines")
|
||||||
self.assertEqual(created_followup.followupattachment_set.count(), 2)
|
self.assertEqual(created_followup.followupattachment_set.count(), 2)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
created_followup.followupattachment_set.first().filename, 'doc.pdf')
|
created_followup.followupattachment_set.first().filename, "doc.pdf"
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
created_followup.followupattachment_set.first().mime_type, 'application/pdf')
|
created_followup.followupattachment_set.first().mime_type, "application/pdf"
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
created_followup.followupattachment_set.last().filename, 'file.jpg')
|
created_followup.followupattachment_set.last().filename, "file.jpg"
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
created_followup.followupattachment_set.last().mime_type, 'image/jpg')
|
created_followup.followupattachment_set.last().mime_type, "image/jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserTicketTest(APITestCase):
|
class UserTicketTest(APITestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.queue = Queue.objects.create(title='Test queue')
|
self.queue = Queue.objects.create(title="Test queue")
|
||||||
self.user = User.objects.create_user(username='test')
|
self.user = User.objects.create_user(username="test")
|
||||||
self.client.force_authenticate(self.user)
|
self.client.force_authenticate(self.user)
|
||||||
|
|
||||||
def test_get_user_tickets(self):
|
def test_get_user_tickets(self):
|
||||||
user = User.objects.create_user(username='test2', email="foo@example.com")
|
user = User.objects.create_user(username="test2", email="foo@example.com")
|
||||||
ticket_1 = Ticket.objects.create(
|
ticket_1 = Ticket.objects.create(
|
||||||
queue=self.queue, title='Test 1',
|
queue=self.queue, title="Test 1", submitter_email="foo@example.com"
|
||||||
submitter_email="foo@example.com")
|
)
|
||||||
ticket_2 = Ticket.objects.create(
|
ticket_2 = Ticket.objects.create(
|
||||||
queue=self.queue, title='Test 2',
|
queue=self.queue, title="Test 2", submitter_email="bar@example.com"
|
||||||
submitter_email="bar@example.com")
|
)
|
||||||
ticket_3 = Ticket.objects.create(
|
ticket_3 = Ticket.objects.create(
|
||||||
queue=self.queue, title='Test 3',
|
queue=self.queue, title="Test 3", submitter_email="foo@example.com"
|
||||||
submitter_email="foo@example.com")
|
)
|
||||||
self.client.force_authenticate(user)
|
self.client.force_authenticate(user)
|
||||||
response = self.client.get('/api/user_tickets/')
|
response = self.client.get("/api/user_tickets/")
|
||||||
self.assertEqual(response.status_code, HTTP_200_OK)
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data["results"]), 2)
|
self.assertEqual(len(response.data["results"]), 2)
|
||||||
self.assertEqual(response.data["results"][0]['id'], ticket_3.id)
|
self.assertEqual(response.data["results"][0]["id"], ticket_3.id)
|
||||||
self.assertEqual(response.data["results"][1]['id'], ticket_1.id)
|
self.assertEqual(response.data["results"][1]["id"], ticket_1.id)
|
||||||
|
|
||||||
def test_staff_user(self):
|
def test_staff_user(self):
|
||||||
staff_user = User.objects.create_user(username='test2', is_staff=True, email="staff@example.com")
|
staff_user = User.objects.create_user(
|
||||||
|
username="test2", is_staff=True, email="staff@example.com"
|
||||||
|
)
|
||||||
ticket_1 = Ticket.objects.create(
|
ticket_1 = Ticket.objects.create(
|
||||||
queue=self.queue, title='Test 1',
|
queue=self.queue, title="Test 1", submitter_email="staff@example.com"
|
||||||
submitter_email="staff@example.com")
|
)
|
||||||
ticket_2 = Ticket.objects.create(
|
ticket_2 = Ticket.objects.create(
|
||||||
queue=self.queue, title='Test 2',
|
queue=self.queue, title="Test 2", submitter_email="foo@example.com"
|
||||||
submitter_email="foo@example.com")
|
)
|
||||||
self.client.force_authenticate(staff_user)
|
self.client.force_authenticate(staff_user)
|
||||||
response = self.client.get('/api/user_tickets/')
|
response = self.client.get("/api/user_tickets/")
|
||||||
self.assertEqual(response.status_code, HTTP_200_OK)
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data["results"]), 1)
|
self.assertEqual(len(response.data["results"]), 1)
|
||||||
|
|
||||||
def test_not_logged_in_user(self):
|
def test_not_logged_in_user(self):
|
||||||
ticket_1 = Ticket.objects.create(
|
ticket_1 = Ticket.objects.create(
|
||||||
queue=self.queue, title='Test 1',
|
queue=self.queue, title="Test 1", submitter_email="ex@example.com"
|
||||||
submitter_email="ex@example.com")
|
)
|
||||||
self.client.logout()
|
self.client.logout()
|
||||||
response = self.client.get('/api/user_tickets/')
|
response = self.client.get("/api/user_tickets/")
|
||||||
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,162 +12,161 @@ from unittest import mock
|
|||||||
from unittest.case import skip
|
from unittest.case import skip
|
||||||
|
|
||||||
|
|
||||||
MEDIA_DIR = os.path.join(gettempdir(), 'helpdesk_test_media')
|
MEDIA_DIR = os.path.join(gettempdir(), "helpdesk_test_media")
|
||||||
|
|
||||||
|
|
||||||
@override_settings(MEDIA_ROOT=MEDIA_DIR)
|
@override_settings(MEDIA_ROOT=MEDIA_DIR)
|
||||||
class AttachmentIntegrationTests(TestCase):
|
class AttachmentIntegrationTests(TestCase):
|
||||||
|
fixtures = ["emailtemplate.json"]
|
||||||
fixtures = ['emailtemplate.json']
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.queue_public = models.Queue.objects.create(
|
self.queue_public = models.Queue.objects.create(
|
||||||
title='Public Queue',
|
title="Public Queue",
|
||||||
slug='pub_q',
|
slug="pub_q",
|
||||||
allow_public_submission=True,
|
allow_public_submission=True,
|
||||||
new_ticket_cc='new.public@example.com',
|
new_ticket_cc="new.public@example.com",
|
||||||
updated_ticket_cc='update.public@example.com',
|
updated_ticket_cc="update.public@example.com",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.queue_private = models.Queue.objects.create(
|
self.queue_private = models.Queue.objects.create(
|
||||||
title='Private Queue',
|
title="Private Queue",
|
||||||
slug='priv_q',
|
slug="priv_q",
|
||||||
allow_public_submission=False,
|
allow_public_submission=False,
|
||||||
new_ticket_cc='new.private@example.com',
|
new_ticket_cc="new.private@example.com",
|
||||||
updated_ticket_cc='update.private@example.com',
|
updated_ticket_cc="update.private@example.com",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.ticket_data = {
|
self.ticket_data = {
|
||||||
'title': 'Test Ticket Title',
|
"title": "Test Ticket Title",
|
||||||
'body': 'Test Ticket Desc',
|
"body": "Test Ticket Desc",
|
||||||
'priority': 3,
|
"priority": 3,
|
||||||
'submitter_email': 'submitter@example.com',
|
"submitter_email": "submitter@example.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_create_pub_ticket_with_attachment(self):
|
def test_create_pub_ticket_with_attachment(self):
|
||||||
test_file = SimpleUploadedFile(
|
test_file = SimpleUploadedFile(
|
||||||
'test_att.txt', b'attached file content', 'text/plain')
|
"test_att.txt", b"attached file content", "text/plain"
|
||||||
|
)
|
||||||
post_data = self.ticket_data.copy()
|
post_data = self.ticket_data.copy()
|
||||||
post_data.update({
|
post_data.update(
|
||||||
'queue': self.queue_public.id,
|
{
|
||||||
'attachment': test_file,
|
"queue": self.queue_public.id,
|
||||||
})
|
"attachment": test_file,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure ticket form submits with attachment successfully
|
# Ensure ticket form submits with attachment successfully
|
||||||
response = self.client.post(
|
response = self.client.post(reverse("helpdesk:home"), post_data, follow=True)
|
||||||
reverse('helpdesk:home'), post_data, follow=True)
|
|
||||||
self.assertContains(response, test_file.name)
|
self.assertContains(response, test_file.name)
|
||||||
|
|
||||||
# Ensure attachment is available with correct content
|
# Ensure attachment is available with correct content
|
||||||
att = models.FollowUpAttachment.objects.get(
|
att = models.FollowUpAttachment.objects.get(
|
||||||
followup__ticket=response.context['ticket'])
|
followup__ticket=response.context["ticket"]
|
||||||
|
)
|
||||||
with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk:
|
with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk:
|
||||||
disk_content = file_on_disk.read()
|
disk_content = file_on_disk.read()
|
||||||
self.assertEqual(disk_content, 'attached file content')
|
self.assertEqual(disk_content, "attached file content")
|
||||||
|
|
||||||
def test_create_pub_ticket_with_attachment_utf8(self):
|
def test_create_pub_ticket_with_attachment_utf8(self):
|
||||||
test_file = SimpleUploadedFile(
|
test_file = SimpleUploadedFile("ß°äöü.txt", "โจ".encode("utf-8"), "text/utf-8")
|
||||||
'ß°äöü.txt', 'โจ'.encode('utf-8'), 'text/utf-8')
|
|
||||||
post_data = self.ticket_data.copy()
|
post_data = self.ticket_data.copy()
|
||||||
post_data.update({
|
post_data.update(
|
||||||
'queue': self.queue_public.id,
|
{
|
||||||
'attachment': test_file,
|
"queue": self.queue_public.id,
|
||||||
})
|
"attachment": test_file,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure ticket form submits with attachment successfully
|
# Ensure ticket form submits with attachment successfully
|
||||||
response = self.client.post(
|
response = self.client.post(reverse("helpdesk:home"), post_data, follow=True)
|
||||||
reverse('helpdesk:home'), post_data, follow=True)
|
|
||||||
self.assertContains(response, test_file.name)
|
self.assertContains(response, test_file.name)
|
||||||
|
|
||||||
# Ensure attachment is available with correct content
|
# Ensure attachment is available with correct content
|
||||||
att = models.FollowUpAttachment.objects.get(
|
att = models.FollowUpAttachment.objects.get(
|
||||||
followup__ticket=response.context['ticket'])
|
followup__ticket=response.context["ticket"]
|
||||||
with open(os.path.join(MEDIA_DIR, att.file.name), encoding="utf-8") as file_on_disk:
|
)
|
||||||
disk_content = smart_str(file_on_disk.read(), 'utf-8')
|
with open(
|
||||||
self.assertEqual(disk_content, 'โจ')
|
os.path.join(MEDIA_DIR, att.file.name), encoding="utf-8"
|
||||||
|
) as file_on_disk:
|
||||||
|
disk_content = smart_str(file_on_disk.read(), "utf-8")
|
||||||
|
self.assertEqual(disk_content, "โจ")
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(models.FollowUp, 'save', autospec=True)
|
@mock.patch.object(models.FollowUp, "save", autospec=True)
|
||||||
@mock.patch.object(models.FollowUpAttachment, 'save', autospec=True)
|
@mock.patch.object(models.FollowUpAttachment, "save", autospec=True)
|
||||||
@mock.patch.object(models.Ticket, 'save', autospec=True)
|
@mock.patch.object(models.Ticket, "save", autospec=True)
|
||||||
@mock.patch.object(models.Queue, 'save', autospec=True)
|
@mock.patch.object(models.Queue, "save", autospec=True)
|
||||||
class AttachmentUnitTests(TestCase):
|
class AttachmentUnitTests(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.file_attrs = {
|
self.file_attrs = {
|
||||||
'filename': '°ßäöü.txt',
|
"filename": "°ßäöü.txt",
|
||||||
'content': 'โจ'.encode('utf-8'),
|
"content": "โจ".encode("utf-8"),
|
||||||
'content-type': 'text/utf8',
|
"content-type": "text/utf8",
|
||||||
}
|
}
|
||||||
self.test_file = SimpleUploadedFile.from_dict(self.file_attrs)
|
self.test_file = SimpleUploadedFile.from_dict(self.file_attrs)
|
||||||
self.follow_up = models.FollowUp.objects.create(
|
self.follow_up = models.FollowUp.objects.create(
|
||||||
ticket=models.Ticket.objects.create(
|
ticket=models.Ticket.objects.create(queue=models.Queue.objects.create())
|
||||||
queue=models.Queue.objects.create()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@skip("Rework with model relocation")
|
@skip("Rework with model relocation")
|
||||||
def test_unicode_attachment_filename(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
|
def test_unicode_attachment_filename(
|
||||||
""" check utf-8 data is parsed correctly """
|
self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save
|
||||||
filename, fileobj = lib.process_attachments(
|
):
|
||||||
self.follow_up, [self.test_file])[0]
|
"""check utf-8 data is parsed correctly"""
|
||||||
|
filename, fileobj = lib.process_attachments(self.follow_up, [self.test_file])[0]
|
||||||
mock_att_save.assert_called_with(
|
mock_att_save.assert_called_with(
|
||||||
file=self.test_file,
|
file=self.test_file,
|
||||||
filename=self.file_attrs['filename'],
|
filename=self.file_attrs["filename"],
|
||||||
mime_type=self.file_attrs['content-type'],
|
mime_type=self.file_attrs["content-type"],
|
||||||
size=len(self.file_attrs['content']),
|
size=len(self.file_attrs["content"]),
|
||||||
followup=self.follow_up
|
|
||||||
)
|
|
||||||
self.assertEqual(filename, self.file_attrs['filename'])
|
|
||||||
|
|
||||||
def test_autofill(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
|
|
||||||
""" check utf-8 data is parsed correctly """
|
|
||||||
obj = models.FollowUpAttachment.objects.create(
|
|
||||||
followup=self.follow_up,
|
followup=self.follow_up,
|
||||||
file=self.test_file
|
)
|
||||||
|
self.assertEqual(filename, self.file_attrs["filename"])
|
||||||
|
|
||||||
|
def test_autofill(
|
||||||
|
self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save
|
||||||
|
):
|
||||||
|
"""check utf-8 data is parsed correctly"""
|
||||||
|
obj = models.FollowUpAttachment.objects.create(
|
||||||
|
followup=self.follow_up, file=self.test_file
|
||||||
)
|
)
|
||||||
obj.save()
|
obj.save()
|
||||||
self.assertEqual(obj.file.name, self.file_attrs['filename'])
|
self.assertEqual(obj.file.name, self.file_attrs["filename"])
|
||||||
self.assertEqual(obj.file.size, len(self.file_attrs['content']))
|
self.assertEqual(obj.file.size, len(self.file_attrs["content"]))
|
||||||
self.assertEqual(obj.file.file.content_type, "text/utf8")
|
self.assertEqual(obj.file.file.content_type, "text/utf8")
|
||||||
|
|
||||||
def test_kbi_attachment(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
|
def test_kbi_attachment(
|
||||||
""" check utf-8 data is parsed correctly """
|
self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save
|
||||||
|
):
|
||||||
|
"""check utf-8 data is parsed correctly"""
|
||||||
|
|
||||||
kbcategory = models.KBCategory.objects.create(
|
kbcategory = models.KBCategory.objects.create(
|
||||||
title="Title",
|
title="Title", slug="slug", description="Description"
|
||||||
slug="slug",
|
|
||||||
description="Description"
|
|
||||||
)
|
)
|
||||||
kbitem = models.KBItem.objects.create(
|
kbitem = models.KBItem.objects.create(
|
||||||
category=kbcategory,
|
category=kbcategory, title="Title", question="Question", answer="Answer"
|
||||||
title="Title",
|
|
||||||
question="Question",
|
|
||||||
answer="Answer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
obj = models.KBIAttachment.objects.create(
|
obj = models.KBIAttachment.objects.create(kbitem=kbitem, file=self.test_file)
|
||||||
kbitem=kbitem,
|
|
||||||
file=self.test_file
|
|
||||||
)
|
|
||||||
obj.save()
|
obj.save()
|
||||||
self.assertEqual(obj.filename, self.file_attrs['filename'])
|
self.assertEqual(obj.filename, self.file_attrs["filename"])
|
||||||
self.assertEqual(obj.file.size, len(self.file_attrs['content']))
|
self.assertEqual(obj.file.size, len(self.file_attrs["content"]))
|
||||||
self.assertEqual(obj.mime_type, "text/plain")
|
self.assertEqual(obj.mime_type, "text/plain")
|
||||||
|
|
||||||
@skip("model in lib not patched")
|
@skip("model in lib not patched")
|
||||||
@override_settings(MEDIA_ROOT=MEDIA_DIR)
|
@override_settings(MEDIA_ROOT=MEDIA_DIR)
|
||||||
def test_unicode_filename_to_filesystem(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
|
def test_unicode_filename_to_filesystem(
|
||||||
""" don't mock saving to filesystem to test file renames caused by storage layer """
|
self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save
|
||||||
filename, fileobj = lib.process_attachments(
|
):
|
||||||
self.follow_up, [self.test_file])[0]
|
"""don't mock saving to filesystem to test file renames caused by storage layer"""
|
||||||
|
filename, fileobj = lib.process_attachments(self.follow_up, [self.test_file])[0]
|
||||||
# Attachment object was zeroth positional arg (i.e. self) of att.save
|
# Attachment object was zeroth positional arg (i.e. self) of att.save
|
||||||
# call
|
# call
|
||||||
attachment_obj = mock_att_save.return_value
|
attachment_obj = mock_att_save.return_value
|
||||||
|
|
||||||
mock_att_save.assert_called_once_with(attachment_obj)
|
mock_att_save.assert_called_once_with(attachment_obj)
|
||||||
self.assertIsInstance(attachment_obj, models.FollowUpAttachment)
|
self.assertIsInstance(attachment_obj, models.FollowUpAttachment)
|
||||||
self.assertEqual(attachment_obj.filename, self.file_attrs['filename'])
|
self.assertEqual(attachment_obj.filename, self.file_attrs["filename"])
|
||||||
|
|
||||||
|
|
||||||
def tearDownModule():
|
def tearDownModule():
|
||||||
|
@ -8,245 +8,254 @@ from helpdesk.models import Checklist, ChecklistTask, ChecklistTemplate, Queue,
|
|||||||
class TicketChecklistTestCase(TestCase):
|
class TicketChecklistTestCase(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
user = get_user_model().objects.create_user('User', password='pass')
|
user = get_user_model().objects.create_user("User", password="pass")
|
||||||
user.is_staff = True
|
user.is_staff = True
|
||||||
user.save()
|
user.save()
|
||||||
cls.user = user
|
cls.user = user
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.client.login(username='User', password='pass')
|
self.client.login(username="User", password="pass")
|
||||||
|
|
||||||
self.ticket = Ticket.objects.create(queue=Queue.objects.create(title='Queue', slug='queue'))
|
self.ticket = Ticket.objects.create(
|
||||||
|
queue=Queue.objects.create(title="Queue", slug="queue")
|
||||||
|
)
|
||||||
|
|
||||||
def test_create_checklist(self):
|
def test_create_checklist(self):
|
||||||
self.assertEqual(self.ticket.checklists.count(), 0)
|
self.assertEqual(self.ticket.checklists.count(), 0)
|
||||||
checklist_name = 'test empty checklist'
|
checklist_name = "test empty checklist"
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse('helpdesk:view', kwargs={'ticket_id': self.ticket.id}),
|
reverse("helpdesk:view", kwargs={"ticket_id": self.ticket.id}),
|
||||||
data={'name': checklist_name},
|
data={"name": checklist_name},
|
||||||
follow=True
|
follow=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/checklist_form.html')
|
self.assertTemplateUsed(response, "helpdesk/checklist_form.html")
|
||||||
self.assertContains(response, checklist_name)
|
self.assertContains(response, checklist_name)
|
||||||
|
|
||||||
self.assertEqual(self.ticket.checklists.count(), 1)
|
self.assertEqual(self.ticket.checklists.count(), 1)
|
||||||
|
|
||||||
def test_create_checklist_from_template(self):
|
def test_create_checklist_from_template(self):
|
||||||
self.assertEqual(self.ticket.checklists.count(), 0)
|
self.assertEqual(self.ticket.checklists.count(), 0)
|
||||||
checklist_name = 'test checklist from template'
|
checklist_name = "test checklist from template"
|
||||||
|
|
||||||
checklist_template = ChecklistTemplate.objects.create(
|
checklist_template = ChecklistTemplate.objects.create(
|
||||||
name='Test template',
|
name="Test template", task_list=["first", "second", "last"]
|
||||||
task_list=['first', 'second', 'last']
|
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse('helpdesk:view', kwargs={'ticket_id': self.ticket.id}),
|
reverse("helpdesk:view", kwargs={"ticket_id": self.ticket.id}),
|
||||||
data={'name': checklist_name, 'checklist_template': checklist_template.id},
|
data={"name": checklist_name, "checklist_template": checklist_template.id},
|
||||||
follow=True
|
follow=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/checklist_form.html')
|
self.assertTemplateUsed(response, "helpdesk/checklist_form.html")
|
||||||
self.assertContains(response, checklist_name)
|
self.assertContains(response, checklist_name)
|
||||||
|
|
||||||
self.assertEqual(self.ticket.checklists.count(), 1)
|
self.assertEqual(self.ticket.checklists.count(), 1)
|
||||||
created_checklist = self.ticket.checklists.get()
|
created_checklist = self.ticket.checklists.get()
|
||||||
self.assertEqual(created_checklist.tasks.count(), 3)
|
self.assertEqual(created_checklist.tasks.count(), 3)
|
||||||
self.assertEqual(created_checklist.tasks.all()[0].description, 'first')
|
self.assertEqual(created_checklist.tasks.all()[0].description, "first")
|
||||||
self.assertEqual(created_checklist.tasks.all()[1].description, 'second')
|
self.assertEqual(created_checklist.tasks.all()[1].description, "second")
|
||||||
self.assertEqual(created_checklist.tasks.all()[2].description, 'last')
|
self.assertEqual(created_checklist.tasks.all()[2].description, "last")
|
||||||
|
|
||||||
def test_edit_checklist(self):
|
def test_edit_checklist(self):
|
||||||
checklist = self.ticket.checklists.create(name='Test checklist')
|
checklist = self.ticket.checklists.create(name="Test checklist")
|
||||||
first_task = checklist.tasks.create(description='First task', position=1)
|
first_task = checklist.tasks.create(description="First task", position=1)
|
||||||
checklist.tasks.create(description='To delete task', position=2)
|
checklist.tasks.create(description="To delete task", position=2)
|
||||||
|
|
||||||
url = reverse('helpdesk:edit_ticket_checklist', kwargs={
|
url = reverse(
|
||||||
'ticket_id': self.ticket.id,
|
"helpdesk:edit_ticket_checklist",
|
||||||
'checklist_id': checklist.id,
|
kwargs={
|
||||||
})
|
"ticket_id": self.ticket.id,
|
||||||
|
"checklist_id": checklist.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/checklist_form.html')
|
self.assertTemplateUsed(response, "helpdesk/checklist_form.html")
|
||||||
self.assertContains(response, 'Test checklist')
|
self.assertContains(response, "Test checklist")
|
||||||
self.assertContains(response, 'First task')
|
self.assertContains(response, "First task")
|
||||||
self.assertContains(response, 'To delete task')
|
self.assertContains(response, "To delete task")
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
url,
|
url,
|
||||||
data={
|
data={
|
||||||
'name': 'New name',
|
"name": "New name",
|
||||||
'tasks-TOTAL_FORMS': 3,
|
"tasks-TOTAL_FORMS": 3,
|
||||||
'tasks-INITIAL_FORMS': 2,
|
"tasks-INITIAL_FORMS": 2,
|
||||||
'tasks-0-id': '1',
|
"tasks-0-id": "1",
|
||||||
'tasks-0-description': 'First task edited',
|
"tasks-0-description": "First task edited",
|
||||||
'tasks-0-position': '2',
|
"tasks-0-position": "2",
|
||||||
'tasks-1-id': '2',
|
"tasks-1-id": "2",
|
||||||
'tasks-1-description': 'To delete task',
|
"tasks-1-description": "To delete task",
|
||||||
'tasks-1-DELETE': 'on',
|
"tasks-1-DELETE": "on",
|
||||||
'tasks-1-position': '2',
|
"tasks-1-position": "2",
|
||||||
'tasks-2-description': 'New first task',
|
"tasks-2-description": "New first task",
|
||||||
'tasks-2-position': '1',
|
"tasks-2-position": "1",
|
||||||
},
|
},
|
||||||
follow=True
|
follow=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/ticket.html')
|
self.assertTemplateUsed(response, "helpdesk/ticket.html")
|
||||||
|
|
||||||
checklist.refresh_from_db()
|
checklist.refresh_from_db()
|
||||||
self.assertEqual(checklist.name, 'New name')
|
self.assertEqual(checklist.name, "New name")
|
||||||
self.assertEqual(checklist.tasks.count(), 2)
|
self.assertEqual(checklist.tasks.count(), 2)
|
||||||
first_task.refresh_from_db()
|
first_task.refresh_from_db()
|
||||||
self.assertEqual(first_task.description, 'First task edited')
|
self.assertEqual(first_task.description, "First task edited")
|
||||||
self.assertEqual(checklist.tasks.all()[0].description, 'New first task')
|
self.assertEqual(checklist.tasks.all()[0].description, "New first task")
|
||||||
self.assertEqual(checklist.tasks.all()[1].description, 'First task edited')
|
self.assertEqual(checklist.tasks.all()[1].description, "First task edited")
|
||||||
|
|
||||||
def test_delete_checklist(self):
|
def test_delete_checklist(self):
|
||||||
checklist = self.ticket.checklists.create(name='Test checklist')
|
checklist = self.ticket.checklists.create(name="Test checklist")
|
||||||
checklist.tasks.create(description='First task', position=1)
|
checklist.tasks.create(description="First task", position=1)
|
||||||
self.assertEqual(Checklist.objects.count(), 1)
|
self.assertEqual(Checklist.objects.count(), 1)
|
||||||
self.assertEqual(ChecklistTask.objects.count(), 1)
|
self.assertEqual(ChecklistTask.objects.count(), 1)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
'helpdesk:delete_ticket_checklist',
|
"helpdesk:delete_ticket_checklist",
|
||||||
kwargs={'ticket_id': self.ticket.id, 'checklist_id': checklist.id}
|
kwargs={"ticket_id": self.ticket.id, "checklist_id": checklist.id},
|
||||||
),
|
),
|
||||||
follow=True
|
follow=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/ticket.html')
|
self.assertTemplateUsed(response, "helpdesk/ticket.html")
|
||||||
|
|
||||||
self.assertEqual(Checklist.objects.count(), 0)
|
self.assertEqual(Checklist.objects.count(), 0)
|
||||||
self.assertEqual(ChecklistTask.objects.count(), 0)
|
self.assertEqual(ChecklistTask.objects.count(), 0)
|
||||||
|
|
||||||
def test_mark_task_as_done(self):
|
def test_mark_task_as_done(self):
|
||||||
checklist = self.ticket.checklists.create(name='Test checklist')
|
checklist = self.ticket.checklists.create(name="Test checklist")
|
||||||
task = checklist.tasks.create(description='Task', position=1)
|
task = checklist.tasks.create(description="Task", position=1)
|
||||||
self.assertIsNone(task.completion_date)
|
self.assertIsNone(task.completion_date)
|
||||||
|
|
||||||
self.assertEqual(self.ticket.followup_set.count(), 0)
|
self.assertEqual(self.ticket.followup_set.count(), 0)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse('helpdesk:update', kwargs={'ticket_id': self.ticket.id}),
|
reverse("helpdesk:update", kwargs={"ticket_id": self.ticket.id}),
|
||||||
data={
|
data={f"checklist-{checklist.id}": task.id},
|
||||||
f'checklist-{checklist.id}': task.id
|
follow=True,
|
||||||
},
|
|
||||||
follow=True
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/ticket.html')
|
self.assertTemplateUsed(response, "helpdesk/ticket.html")
|
||||||
|
|
||||||
self.assertEqual(self.ticket.followup_set.count(), 1)
|
self.assertEqual(self.ticket.followup_set.count(), 1)
|
||||||
followup = self.ticket.followup_set.get()
|
followup = self.ticket.followup_set.get()
|
||||||
self.assertEqual(followup.ticketchange_set.count(), 1)
|
self.assertEqual(followup.ticketchange_set.count(), 1)
|
||||||
self.assertEqual(followup.ticketchange_set.get().old_value, 'To do')
|
self.assertEqual(followup.ticketchange_set.get().old_value, "To do")
|
||||||
self.assertEqual(followup.ticketchange_set.get().new_value, 'Completed')
|
self.assertEqual(followup.ticketchange_set.get().new_value, "Completed")
|
||||||
|
|
||||||
task.refresh_from_db()
|
task.refresh_from_db()
|
||||||
self.assertIsNotNone(task.completion_date)
|
self.assertIsNotNone(task.completion_date)
|
||||||
|
|
||||||
def test_mark_task_as_undone(self):
|
def test_mark_task_as_undone(self):
|
||||||
checklist = self.ticket.checklists.create(name='Test checklist')
|
checklist = self.ticket.checklists.create(name="Test checklist")
|
||||||
task = checklist.tasks.create(description='Task', position=1, completion_date=datetime(2023, 5, 1))
|
task = checklist.tasks.create(
|
||||||
|
description="Task", position=1, completion_date=datetime(2023, 5, 1)
|
||||||
|
)
|
||||||
self.assertIsNotNone(task.completion_date)
|
self.assertIsNotNone(task.completion_date)
|
||||||
|
|
||||||
self.assertEqual(self.ticket.followup_set.count(), 0)
|
self.assertEqual(self.ticket.followup_set.count(), 0)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse('helpdesk:update', kwargs={'ticket_id': self.ticket.id}),
|
reverse("helpdesk:update", kwargs={"ticket_id": self.ticket.id}),
|
||||||
follow=True
|
follow=True,
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/ticket.html')
|
self.assertTemplateUsed(response, "helpdesk/ticket.html")
|
||||||
|
|
||||||
self.assertEqual(self.ticket.followup_set.count(), 1)
|
self.assertEqual(self.ticket.followup_set.count(), 1)
|
||||||
followup = self.ticket.followup_set.get()
|
followup = self.ticket.followup_set.get()
|
||||||
self.assertEqual(followup.ticketchange_set.count(), 1)
|
self.assertEqual(followup.ticketchange_set.count(), 1)
|
||||||
self.assertEqual(followup.ticketchange_set.get().old_value, 'Completed')
|
self.assertEqual(followup.ticketchange_set.get().old_value, "Completed")
|
||||||
self.assertEqual(followup.ticketchange_set.get().new_value, 'To do')
|
self.assertEqual(followup.ticketchange_set.get().new_value, "To do")
|
||||||
|
|
||||||
task.refresh_from_db()
|
task.refresh_from_db()
|
||||||
self.assertIsNone(task.completion_date)
|
self.assertIsNone(task.completion_date)
|
||||||
|
|
||||||
def test_display_checklist_templates(self):
|
def test_display_checklist_templates(self):
|
||||||
ChecklistTemplate.objects.create(
|
ChecklistTemplate.objects.create(
|
||||||
name='Test checklist template',
|
name="Test checklist template", task_list=["first", "second", "third"]
|
||||||
task_list=['first', 'second', 'third']
|
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.get(reverse('helpdesk:checklist_templates'))
|
response = self.client.get(reverse("helpdesk:checklist_templates"))
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html')
|
self.assertTemplateUsed(response, "helpdesk/checklist_templates.html")
|
||||||
self.assertContains(response, 'Test checklist template')
|
self.assertContains(response, "Test checklist template")
|
||||||
self.assertContains(response, '3 tasks')
|
self.assertContains(response, "3 tasks")
|
||||||
|
|
||||||
def test_create_checklist_template(self):
|
def test_create_checklist_template(self):
|
||||||
self.assertEqual(ChecklistTemplate.objects.count(), 0)
|
self.assertEqual(ChecklistTemplate.objects.count(), 0)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse('helpdesk:checklist_templates'),
|
reverse("helpdesk:checklist_templates"),
|
||||||
data={
|
data={
|
||||||
'name': 'Test checklist template',
|
"name": "Test checklist template",
|
||||||
'task_list': '["first", "second", "third"]'
|
"task_list": '["first", "second", "third"]',
|
||||||
},
|
},
|
||||||
follow=True
|
follow=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html')
|
self.assertTemplateUsed(response, "helpdesk/checklist_templates.html")
|
||||||
|
|
||||||
self.assertEqual(ChecklistTemplate.objects.count(), 1)
|
self.assertEqual(ChecklistTemplate.objects.count(), 1)
|
||||||
checklist_template = ChecklistTemplate.objects.get()
|
checklist_template = ChecklistTemplate.objects.get()
|
||||||
self.assertEqual(checklist_template.name, 'Test checklist template')
|
self.assertEqual(checklist_template.name, "Test checklist template")
|
||||||
self.assertEqual(checklist_template.task_list, ['first', 'second', 'third'])
|
self.assertEqual(checklist_template.task_list, ["first", "second", "third"])
|
||||||
|
|
||||||
def test_edit_checklist_template(self):
|
def test_edit_checklist_template(self):
|
||||||
checklist_template = ChecklistTemplate.objects.create(
|
checklist_template = ChecklistTemplate.objects.create(
|
||||||
name='Test checklist template',
|
name="Test checklist template", task_list=["first", "second", "third"]
|
||||||
task_list=['first', 'second', 'third']
|
|
||||||
)
|
)
|
||||||
self.assertEqual(ChecklistTemplate.objects.count(), 1)
|
self.assertEqual(ChecklistTemplate.objects.count(), 1)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse('helpdesk:edit_checklist_template', kwargs={'checklist_template_id': checklist_template.id}),
|
reverse(
|
||||||
|
"helpdesk:edit_checklist_template",
|
||||||
|
kwargs={"checklist_template_id": checklist_template.id},
|
||||||
|
),
|
||||||
data={
|
data={
|
||||||
'name': 'New checklist template',
|
"name": "New checklist template",
|
||||||
'task_list': '["new first", "second", "third", "last"]'
|
"task_list": '["new first", "second", "third", "last"]',
|
||||||
},
|
},
|
||||||
follow=True
|
follow=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html')
|
self.assertTemplateUsed(response, "helpdesk/checklist_templates.html")
|
||||||
|
|
||||||
self.assertEqual(ChecklistTemplate.objects.count(), 1)
|
self.assertEqual(ChecklistTemplate.objects.count(), 1)
|
||||||
checklist_template.refresh_from_db()
|
checklist_template.refresh_from_db()
|
||||||
self.assertEqual(checklist_template.name, 'New checklist template')
|
self.assertEqual(checklist_template.name, "New checklist template")
|
||||||
self.assertEqual(checklist_template.task_list, ['new first', 'second', 'third', 'last'])
|
self.assertEqual(
|
||||||
|
checklist_template.task_list, ["new first", "second", "third", "last"]
|
||||||
|
)
|
||||||
|
|
||||||
def test_delete_checklist_template(self):
|
def test_delete_checklist_template(self):
|
||||||
checklist_template = ChecklistTemplate.objects.create(
|
checklist_template = ChecklistTemplate.objects.create(
|
||||||
name='Test checklist template',
|
name="Test checklist template", task_list=["first", "second", "third"]
|
||||||
task_list=['first', 'second', 'third']
|
|
||||||
)
|
)
|
||||||
self.assertEqual(ChecklistTemplate.objects.count(), 1)
|
self.assertEqual(ChecklistTemplate.objects.count(), 1)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse('helpdesk:delete_checklist_template', kwargs={'checklist_template_id': checklist_template.id}),
|
reverse(
|
||||||
follow=True
|
"helpdesk:delete_checklist_template",
|
||||||
|
kwargs={"checklist_template_id": checklist_template.id},
|
||||||
|
),
|
||||||
|
follow=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html')
|
self.assertTemplateUsed(response, "helpdesk/checklist_templates.html")
|
||||||
|
|
||||||
self.assertEqual(ChecklistTemplate.objects.count(), 0)
|
self.assertEqual(ChecklistTemplate.objects.count(), 0)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -37,52 +37,52 @@ class KBTests(TestCase):
|
|||||||
self.user = get_staff_user()
|
self.user = get_staff_user()
|
||||||
|
|
||||||
def test_kb_index(self):
|
def test_kb_index(self):
|
||||||
response = self.client.get(reverse('helpdesk:kb_index'))
|
response = self.client.get(reverse("helpdesk:kb_index"))
|
||||||
self.assertContains(response, 'This is a test category')
|
self.assertContains(response, "This is a test category")
|
||||||
|
|
||||||
def test_kb_category(self):
|
def test_kb_category(self):
|
||||||
response = self.client.get(
|
response = self.client.get(reverse("helpdesk:kb_category", args=("test_cat",)))
|
||||||
reverse('helpdesk:kb_category', args=("test_cat", )))
|
self.assertContains(response, "This is a test category")
|
||||||
self.assertContains(response, 'This is a test category')
|
self.assertContains(response, "KBItem 1")
|
||||||
self.assertContains(response, 'KBItem 1')
|
self.assertContains(response, "KBItem 2")
|
||||||
self.assertContains(response, 'KBItem 2')
|
self.assertContains(response, "Create New Ticket Queue:")
|
||||||
self.assertContains(response, 'Create New Ticket Queue:')
|
self.client.login(username=self.user.get_username(), password="password")
|
||||||
self.client.login(username=self.user.get_username(),
|
response = self.client.get(reverse("helpdesk:kb_category", args=("test_cat",)))
|
||||||
password='password')
|
|
||||||
response = self.client.get(
|
|
||||||
reverse('helpdesk:kb_category', args=("test_cat", )))
|
|
||||||
self.assertContains(response, '<i class="fa fa-thumbs-up fa-lg"></i>')
|
self.assertContains(response, '<i class="fa fa-thumbs-up fa-lg"></i>')
|
||||||
self.assertContains(response, '0 open tickets')
|
self.assertContains(response, "0 open tickets")
|
||||||
ticket = Ticket.objects.create(
|
ticket = Ticket.objects.create(
|
||||||
title="Test ticket",
|
title="Test ticket",
|
||||||
queue=self.queue,
|
queue=self.queue,
|
||||||
kbitem=self.kbitem1,
|
kbitem=self.kbitem1,
|
||||||
)
|
)
|
||||||
ticket.save()
|
ticket.save()
|
||||||
response = self.client.get(
|
response = self.client.get(reverse("helpdesk:kb_category", args=("test_cat",)))
|
||||||
reverse('helpdesk:kb_category', args=("test_cat",)))
|
self.assertContains(response, "1 open tickets")
|
||||||
self.assertContains(response, '1 open tickets')
|
|
||||||
|
|
||||||
def test_kb_vote(self):
|
def test_kb_vote(self):
|
||||||
self.client.login(username=self.user.get_username(),
|
self.client.login(username=self.user.get_username(), password="password")
|
||||||
password='password')
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse('helpdesk:kb_vote', args=(self.kbitem1.pk, "up")), params={})
|
reverse("helpdesk:kb_vote", args=(self.kbitem1.pk, "up")), params={}
|
||||||
cat_url = reverse('helpdesk:kb_category',
|
)
|
||||||
args=("test_cat",)) + "?kbitem=1"
|
cat_url = reverse("helpdesk:kb_category", args=("test_cat",)) + "?kbitem=1"
|
||||||
self.assertRedirects(response, cat_url)
|
self.assertRedirects(response, cat_url)
|
||||||
response = self.client.get(cat_url)
|
response = self.client.get(cat_url)
|
||||||
self.assertContains(response, '1 people found this answer useful of 1')
|
self.assertContains(response, "1 people found this answer useful of 1")
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse('helpdesk:kb_vote', args=(self.kbitem1.pk, "down")), params={})
|
reverse("helpdesk:kb_vote", args=(self.kbitem1.pk, "down")), params={}
|
||||||
|
)
|
||||||
self.assertRedirects(response, cat_url)
|
self.assertRedirects(response, cat_url)
|
||||||
response = self.client.get(cat_url)
|
response = self.client.get(cat_url)
|
||||||
self.assertContains(response, '0 people found this answer useful of 1')
|
self.assertContains(response, "0 people found this answer useful of 1")
|
||||||
|
|
||||||
def test_kb_category_iframe(self):
|
def test_kb_category_iframe(self):
|
||||||
cat_url = reverse('helpdesk:kb_category', args=(
|
cat_url = (
|
||||||
"test_cat",)) + "?kbitem=1&submitter_email=foo@bar.cz&title=lol&"
|
reverse("helpdesk:kb_category", args=("test_cat",))
|
||||||
|
+ "?kbitem=1&submitter_email=foo@bar.cz&title=lol&"
|
||||||
|
)
|
||||||
response = self.client.get(cat_url)
|
response = self.client.get(cat_url)
|
||||||
# Assert that query params are passed on to ticket submit form
|
# Assert that query params are passed on to ticket submit form
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response, "'/tickets/submit/?queue=1&_readonly_fields_=queue&kbitem=1&submitter_email=foo%40bar.cz&title=lol")
|
response,
|
||||||
|
"'/tickets/submit/?queue=1&_readonly_fields_=queue&kbitem=1&submitter_email=foo%40bar.cz&title=lol",
|
||||||
|
)
|
||||||
|
@ -3,43 +3,42 @@ from django.urls import reverse
|
|||||||
|
|
||||||
|
|
||||||
class TestLoginRedirect(TestCase):
|
class TestLoginRedirect(TestCase):
|
||||||
|
@override_settings(LOGIN_URL="/custom/login/")
|
||||||
@override_settings(LOGIN_URL='/custom/login/')
|
|
||||||
def test_custom_login_view_with_url(self):
|
def test_custom_login_view_with_url(self):
|
||||||
"""Test login redirect when LOGIN_URL is set to custom url"""
|
"""Test login redirect when LOGIN_URL is set to custom url"""
|
||||||
response = self.client.get(reverse('helpdesk:login'))
|
response = self.client.get(reverse("helpdesk:login"))
|
||||||
# We expect that that helpdesk:home url is passed as next parameter in
|
# We expect that that helpdesk:home url is passed as next parameter in
|
||||||
# the redirect url, so that the custom login can redirect the browser
|
# the redirect url, so that the custom login can redirect the browser
|
||||||
# back to helpdesk after the login.
|
# back to helpdesk after the login.
|
||||||
home_url = reverse('helpdesk:home')
|
home_url = reverse("helpdesk:home")
|
||||||
expected = '/custom/login/?next={}'.format(home_url)
|
expected = "/custom/login/?next={}".format(home_url)
|
||||||
self.assertRedirects(response, expected, fetch_redirect_response=False)
|
self.assertRedirects(response, expected, fetch_redirect_response=False)
|
||||||
|
|
||||||
@override_settings(LOGIN_URL='/custom/login/')
|
@override_settings(LOGIN_URL="/custom/login/")
|
||||||
def test_custom_login_next_param(self):
|
def test_custom_login_next_param(self):
|
||||||
"""Test that the next url parameter is correctly relayed to custom login"""
|
"""Test that the next url parameter is correctly relayed to custom login"""
|
||||||
next_param = "/redirect/back"
|
next_param = "/redirect/back"
|
||||||
url = reverse('helpdesk:login') + "?next=" + next_param
|
url = reverse("helpdesk:login") + "?next=" + next_param
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
expected = '/custom/login/?next={}'.format(next_param)
|
expected = "/custom/login/?next={}".format(next_param)
|
||||||
self.assertRedirects(response, expected, fetch_redirect_response=False)
|
self.assertRedirects(response, expected, fetch_redirect_response=False)
|
||||||
|
|
||||||
@override_settings(LOGIN_URL='helpdesk:login', SITE_ID=1)
|
@override_settings(LOGIN_URL="helpdesk:login", SITE_ID=1)
|
||||||
def test_default_login_view(self):
|
def test_default_login_view(self):
|
||||||
"""Test that default login is used when LOGIN_URL is helpdesk:login"""
|
"""Test that default login is used when LOGIN_URL is helpdesk:login"""
|
||||||
response = self.client.get(reverse('helpdesk:login'))
|
response = self.client.get(reverse("helpdesk:login"))
|
||||||
self.assertTemplateUsed(response, 'helpdesk/registration/login.html')
|
self.assertTemplateUsed(response, "helpdesk/registration/login.html")
|
||||||
|
|
||||||
@override_settings(LOGIN_URL=None, SITE_ID=1)
|
@override_settings(LOGIN_URL=None, SITE_ID=1)
|
||||||
def test_login_url_none(self):
|
def test_login_url_none(self):
|
||||||
"""Test that default login is used when LOGIN_URL is None"""
|
"""Test that default login is used when LOGIN_URL is None"""
|
||||||
response = self.client.get(reverse('helpdesk:login'))
|
response = self.client.get(reverse("helpdesk:login"))
|
||||||
self.assertTemplateUsed(response, 'helpdesk/registration/login.html')
|
self.assertTemplateUsed(response, "helpdesk/registration/login.html")
|
||||||
|
|
||||||
@override_settings(LOGIN_URL='admin:login', SITE_ID=1)
|
@override_settings(LOGIN_URL="admin:login", SITE_ID=1)
|
||||||
def test_custom_login_view_with_name(self):
|
def test_custom_login_view_with_name(self):
|
||||||
"""Test that LOGIN_URL can be a view name"""
|
"""Test that LOGIN_URL can be a view name"""
|
||||||
response = self.client.get(reverse('helpdesk:login'))
|
response = self.client.get(reverse("helpdesk:login"))
|
||||||
home_url = reverse('helpdesk:home')
|
home_url = reverse("helpdesk:home")
|
||||||
expected = reverse('admin:login') + "?next=" + home_url
|
expected = reverse("admin:login") + "?next=" + home_url
|
||||||
self.assertRedirects(response, expected)
|
self.assertRedirects(response, expected)
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
from helpdesk.models import get_markdown
|
from helpdesk.models import get_markdown
|
||||||
|
|
||||||
|
|
||||||
class MarkDown(SimpleTestCase):
|
class MarkDown(SimpleTestCase):
|
||||||
"""Test work Markdown functional"""
|
"""Test work Markdown functional"""
|
||||||
|
|
||||||
def test_markdown_html_tab(self):
|
def test_markdown_html_tab(self):
|
||||||
expected_value = "<p><div>test<div></p>"
|
expected_value = "<p><div>test<div></p>"
|
||||||
input_value = "<div>test<div>"
|
input_value = "<div>test<div>"
|
||||||
@ -12,7 +12,7 @@ class MarkDown(SimpleTestCase):
|
|||||||
self.assertEqual(output_value, expected_value)
|
self.assertEqual(output_value, expected_value)
|
||||||
|
|
||||||
def test_markdown_nl2br(self):
|
def test_markdown_nl2br(self):
|
||||||
""" warning, after Line 1 - two withespace, esle did't work"""
|
"""warning, after Line 1 - two withespace, esle did't work"""
|
||||||
expected_value = "<p>Line 1<br />\n Line 2</p>"
|
expected_value = "<p>Line 1<br />\n Line 2</p>"
|
||||||
input_value = """Line 1
|
input_value = """Line 1
|
||||||
Line 2"""
|
Line 2"""
|
||||||
|
@ -27,15 +27,15 @@ class KBDisabledTestCase(TestCase):
|
|||||||
"""Test proper rendering of navigation.html by accessing the dashboard"""
|
"""Test proper rendering of navigation.html by accessing the dashboard"""
|
||||||
from django.urls import NoReverseMatch
|
from django.urls import NoReverseMatch
|
||||||
|
|
||||||
self.client.login(username=get_staff_user(
|
self.client.login(username=get_staff_user().get_username(), password="password")
|
||||||
).get_username(), password='password')
|
self.assertRaises(NoReverseMatch, reverse, "helpdesk:kb_index")
|
||||||
self.assertRaises(NoReverseMatch, reverse, 'helpdesk:kb_index')
|
|
||||||
try:
|
try:
|
||||||
response = self.client.get(reverse('helpdesk:dashboard'))
|
response = self.client.get(reverse("helpdesk:dashboard"))
|
||||||
except NoReverseMatch as e:
|
except NoReverseMatch as e:
|
||||||
if 'helpdesk:kb_index' in e.message:
|
if "helpdesk:kb_index" in e.message:
|
||||||
self.fail(
|
self.fail(
|
||||||
"Please verify any unchecked references to helpdesk_kb_index (start with navigation.html)")
|
"Please verify any unchecked references to helpdesk_kb_index (start with navigation.html)"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
@ -47,7 +47,9 @@ class StaffUserTestCaseMixin(object):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.original_setting = helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
self.original_setting = helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
||||||
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = self.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = (
|
||||||
|
self.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
||||||
|
)
|
||||||
self.reload_views()
|
self.reload_views()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
@ -56,16 +58,16 @@ class StaffUserTestCaseMixin(object):
|
|||||||
|
|
||||||
def reload_views(self):
|
def reload_views(self):
|
||||||
try:
|
try:
|
||||||
reload(sys.modules['helpdesk.decorators'])
|
reload(sys.modules["helpdesk.decorators"])
|
||||||
reload(sys.modules['helpdesk.views.staff'])
|
reload(sys.modules["helpdesk.views.staff"])
|
||||||
reload_urlconf()
|
reload_urlconf()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_anonymous_user(self):
|
def test_anonymous_user(self):
|
||||||
"""Access to the dashboard always requires a login"""
|
"""Access to the dashboard always requires a login"""
|
||||||
response = self.client.get(reverse('helpdesk:dashboard'), follow=True)
|
response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/registration/login.html')
|
self.assertTemplateUsed(response, "helpdesk/registration/login.html")
|
||||||
|
|
||||||
|
|
||||||
class NonStaffUsersAllowedTestCase(StaffUserTestCaseMixin, TestCase):
|
class NonStaffUsersAllowedTestCase(StaffUserTestCaseMixin, TestCase):
|
||||||
@ -79,13 +81,16 @@ class NonStaffUsersAllowedTestCase(StaffUserTestCaseMixin, TestCase):
|
|||||||
from helpdesk.decorators import is_helpdesk_staff
|
from helpdesk.decorators import is_helpdesk_staff
|
||||||
|
|
||||||
user = User.objects.create_user(
|
user = User.objects.create_user(
|
||||||
username='henry.wensleydale', password='gouda', email='wensleydale@example.com')
|
username="henry.wensleydale",
|
||||||
|
password="gouda",
|
||||||
|
email="wensleydale@example.com",
|
||||||
|
)
|
||||||
|
|
||||||
self.assertTrue(is_helpdesk_staff(user))
|
self.assertTrue(is_helpdesk_staff(user))
|
||||||
|
|
||||||
self.client.login(username=user.username, password='gouda')
|
self.client.login(username=user.username, password="gouda")
|
||||||
response = self.client.get(reverse('helpdesk:dashboard'), follow=True)
|
response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/dashboard.html')
|
self.assertTemplateUsed(response, "helpdesk/dashboard.html")
|
||||||
|
|
||||||
|
|
||||||
class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
|
class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
|
||||||
@ -96,7 +101,10 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
|
|||||||
super().setUp()
|
super().setUp()
|
||||||
self.non_staff_user_password = "gouda"
|
self.non_staff_user_password = "gouda"
|
||||||
self.non_staff_user = User.objects.create_user(
|
self.non_staff_user = User.objects.create_user(
|
||||||
username='henry.wensleydale', password=self.non_staff_user_password, email='wensleydale@example.com')
|
username="henry.wensleydale",
|
||||||
|
password=self.non_staff_user_password,
|
||||||
|
email="wensleydale@example.com",
|
||||||
|
)
|
||||||
|
|
||||||
def test_staff_user_detection(self):
|
def test_staff_user_detection(self):
|
||||||
"""Staff and non-staff users are correctly identified"""
|
"""Staff and non-staff users are correctly identified"""
|
||||||
@ -111,19 +119,18 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
user = get_staff_user()
|
user = get_staff_user()
|
||||||
self.client.login(username=user.username, password='password')
|
self.client.login(username=user.username, password="password")
|
||||||
response = self.client.get(reverse('helpdesk:dashboard'), follow=True)
|
response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/dashboard.html')
|
self.assertTemplateUsed(response, "helpdesk/dashboard.html")
|
||||||
|
|
||||||
def test_non_staff_cannot_access_dashboard(self):
|
def test_non_staff_cannot_access_dashboard(self):
|
||||||
"""When HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False,
|
"""When HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False,
|
||||||
non-staff users should not be able to access the dashboard.
|
non-staff users should not be able to access the dashboard.
|
||||||
"""
|
"""
|
||||||
user = self.non_staff_user
|
user = self.non_staff_user
|
||||||
self.client.login(username=user.username,
|
self.client.login(username=user.username, password=self.non_staff_user_password)
|
||||||
password=self.non_staff_user_password)
|
response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
|
||||||
response = self.client.get(reverse('helpdesk:dashboard'), follow=True)
|
self.assertTemplateUsed(response, "helpdesk/registration/login.html")
|
||||||
self.assertTemplateUsed(response, 'helpdesk/registration/login.html')
|
|
||||||
|
|
||||||
def test_staff_rss(self):
|
def test_staff_rss(self):
|
||||||
"""If HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False,
|
"""If HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False,
|
||||||
@ -131,9 +138,8 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
|
|||||||
"""
|
"""
|
||||||
user = get_staff_user()
|
user = get_staff_user()
|
||||||
self.client.login(username=user.username, password="password")
|
self.client.login(username=user.username, password="password")
|
||||||
response = self.client.get(
|
response = self.client.get(reverse("helpdesk:rss_unassigned"), follow=True)
|
||||||
reverse('helpdesk:rss_unassigned'), follow=True)
|
self.assertContains(response, "Unassigned Open and Reopened tickets")
|
||||||
self.assertContains(response, 'Unassigned Open and Reopened tickets')
|
|
||||||
|
|
||||||
@override_settings(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE=False)
|
@override_settings(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE=False)
|
||||||
def test_non_staff_cannot_rss(self):
|
def test_non_staff_cannot_rss(self):
|
||||||
@ -141,31 +147,32 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
|
|||||||
non-staff users should not be able to access rss feeds.
|
non-staff users should not be able to access rss feeds.
|
||||||
"""
|
"""
|
||||||
user = self.non_staff_user
|
user = self.non_staff_user
|
||||||
self.client.login(username=user.username,
|
self.client.login(username=user.username, password=self.non_staff_user_password)
|
||||||
password=self.non_staff_user_password)
|
|
||||||
queue = Queue.objects.create(
|
queue = Queue.objects.create(
|
||||||
title="Foo",
|
title="Foo",
|
||||||
slug="test_queue",
|
slug="test_queue",
|
||||||
)
|
)
|
||||||
rss_urls = [
|
rss_urls = [
|
||||||
reverse('helpdesk:rss_user', args=[user.username]),
|
reverse("helpdesk:rss_user", args=[user.username]),
|
||||||
reverse('helpdesk:rss_user_queue', args=[
|
reverse("helpdesk:rss_user_queue", args=[user.username, "test_queue"]),
|
||||||
user.username, 'test_queue']),
|
reverse("helpdesk:rss_queue", args=["test_queue"]),
|
||||||
reverse('helpdesk:rss_queue', args=['test_queue']),
|
reverse("helpdesk:rss_unassigned"),
|
||||||
reverse('helpdesk:rss_unassigned'),
|
reverse("helpdesk:rss_activity"),
|
||||||
reverse('helpdesk:rss_activity'),
|
|
||||||
]
|
]
|
||||||
for rss_url in rss_urls:
|
for rss_url in rss_urls:
|
||||||
response = self.client.get(rss_url, follow=True)
|
response = self.client.get(rss_url, follow=True)
|
||||||
self.assertTemplateUsed(
|
self.assertTemplateUsed(response, "helpdesk/registration/login.html")
|
||||||
response, 'helpdesk/registration/login.html')
|
|
||||||
|
|
||||||
|
|
||||||
class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase):
|
class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def custom_staff_filter(user):
|
def custom_staff_filter(user):
|
||||||
"""Arbitrary user validation function"""
|
"""Arbitrary user validation function"""
|
||||||
return user.is_authenticated and user.is_active and user.username.lower().endswith('wensleydale')
|
return (
|
||||||
|
user.is_authenticated
|
||||||
|
and user.is_active
|
||||||
|
and user.username.lower().endswith("wensleydale")
|
||||||
|
)
|
||||||
|
|
||||||
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = custom_staff_filter
|
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = custom_staff_filter
|
||||||
|
|
||||||
@ -176,25 +183,29 @@ class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase):
|
|||||||
from helpdesk.decorators import is_helpdesk_staff
|
from helpdesk.decorators import is_helpdesk_staff
|
||||||
|
|
||||||
user = User.objects.create_user(
|
user = User.objects.create_user(
|
||||||
username='henry.wensleydale', password='gouda', email='wensleydale@example.com')
|
username="henry.wensleydale",
|
||||||
|
password="gouda",
|
||||||
|
email="wensleydale@example.com",
|
||||||
|
)
|
||||||
|
|
||||||
self.assertTrue(is_helpdesk_staff(user))
|
self.assertTrue(is_helpdesk_staff(user))
|
||||||
|
|
||||||
self.client.login(username=user.username, password='gouda')
|
self.client.login(username=user.username, password="gouda")
|
||||||
response = self.client.get(reverse('helpdesk:dashboard'), follow=True)
|
response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/dashboard.html')
|
self.assertTemplateUsed(response, "helpdesk/dashboard.html")
|
||||||
|
|
||||||
def test_custom_staff_fail(self):
|
def test_custom_staff_fail(self):
|
||||||
from helpdesk.decorators import is_helpdesk_staff
|
from helpdesk.decorators import is_helpdesk_staff
|
||||||
|
|
||||||
user = User.objects.create_user(
|
user = User.objects.create_user(
|
||||||
username='terry.milton', password='frog', email='milton@example.com')
|
username="terry.milton", password="frog", email="milton@example.com"
|
||||||
|
)
|
||||||
|
|
||||||
self.assertFalse(is_helpdesk_staff(user))
|
self.assertFalse(is_helpdesk_staff(user))
|
||||||
|
|
||||||
self.client.login(username=user.username, password='frog')
|
self.client.login(username=user.username, password="frog")
|
||||||
response = self.client.get(reverse('helpdesk:dashboard'), follow=True)
|
response = self.client.get(reverse("helpdesk:dashboard"), follow=True)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/registration/login.html')
|
self.assertTemplateUsed(response, "helpdesk/registration/login.html")
|
||||||
|
|
||||||
|
|
||||||
class HomePageAnonymousUserTestCase(TestCase):
|
class HomePageAnonymousUserTestCase(TestCase):
|
||||||
@ -206,14 +217,14 @@ class HomePageAnonymousUserTestCase(TestCase):
|
|||||||
|
|
||||||
def test_homepage(self):
|
def test_homepage(self):
|
||||||
helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = True
|
helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = True
|
||||||
response = self.client.get(reverse('helpdesk:home'))
|
response = self.client.get(reverse("helpdesk:home"))
|
||||||
self.assertTemplateUsed('helpdesk/public_homepage.html')
|
self.assertTemplateUsed("helpdesk/public_homepage.html")
|
||||||
|
|
||||||
def test_redirect_to_login(self):
|
def test_redirect_to_login(self):
|
||||||
"""Unauthenticated users are redirected to the login page if HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT is True"""
|
"""Unauthenticated users are redirected to the login page if HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT is True"""
|
||||||
helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = True
|
helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = True
|
||||||
response = self.client.get(reverse('helpdesk:home'))
|
response = self.client.get(reverse("helpdesk:home"))
|
||||||
self.assertRedirects(response, reverse('helpdesk:login'))
|
self.assertRedirects(response, reverse("helpdesk:login"))
|
||||||
|
|
||||||
|
|
||||||
class HomePageTestCase(TestCase):
|
class HomePageTestCase(TestCase):
|
||||||
@ -221,17 +232,17 @@ class HomePageTestCase(TestCase):
|
|||||||
self.original_setting = helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
self.original_setting = helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
||||||
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = False
|
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = False
|
||||||
try:
|
try:
|
||||||
reload(sys.modules['helpdesk.views.public'])
|
reload(sys.modules["helpdesk.views.public"])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = self.original_setting
|
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = self.original_setting
|
||||||
reload(sys.modules['helpdesk.views.public'])
|
reload(sys.modules["helpdesk.views.public"])
|
||||||
|
|
||||||
def assertUserRedirectedToView(self, user, view_name):
|
def assertUserRedirectedToView(self, user, view_name):
|
||||||
self.client.login(username=user.username, password='password')
|
self.client.login(username=user.username, password="password")
|
||||||
response = self.client.get(reverse('helpdesk:home'))
|
response = self.client.get(reverse("helpdesk:home"))
|
||||||
self.assertRedirects(response, reverse(view_name))
|
self.assertRedirects(response, reverse(view_name))
|
||||||
self.client.logout()
|
self.client.logout()
|
||||||
|
|
||||||
@ -242,15 +253,16 @@ class HomePageTestCase(TestCase):
|
|||||||
# login_view_ticketlist is False...
|
# login_view_ticketlist is False...
|
||||||
user.usersettings_helpdesk.login_view_ticketlist = False
|
user.usersettings_helpdesk.login_view_ticketlist = False
|
||||||
user.usersettings_helpdesk.save()
|
user.usersettings_helpdesk.save()
|
||||||
self.assertUserRedirectedToView(user, 'helpdesk:dashboard')
|
self.assertUserRedirectedToView(user, "helpdesk:dashboard")
|
||||||
|
|
||||||
def test_no_user_settings_redirect_to_dashboard(self):
|
def test_no_user_settings_redirect_to_dashboard(self):
|
||||||
"""Authenticated users are redirected to the dashboard if user settings are missing"""
|
"""Authenticated users are redirected to the dashboard if user settings are missing"""
|
||||||
from helpdesk.models import UserSettings
|
from helpdesk.models import UserSettings
|
||||||
|
|
||||||
user = get_staff_user()
|
user = get_staff_user()
|
||||||
|
|
||||||
UserSettings.objects.filter(user=user).delete()
|
UserSettings.objects.filter(user=user).delete()
|
||||||
self.assertUserRedirectedToView(user, 'helpdesk:dashboard')
|
self.assertUserRedirectedToView(user, "helpdesk:dashboard")
|
||||||
|
|
||||||
def test_redirect_to_ticket_list(self):
|
def test_redirect_to_ticket_list(self):
|
||||||
"""Authenticated users are redirected to the ticket list based on their user settings"""
|
"""Authenticated users are redirected to the ticket list based on their user settings"""
|
||||||
@ -258,7 +270,7 @@ class HomePageTestCase(TestCase):
|
|||||||
user.usersettings_helpdesk.login_view_ticketlist = True
|
user.usersettings_helpdesk.login_view_ticketlist = True
|
||||||
user.usersettings_helpdesk.save()
|
user.usersettings_helpdesk.save()
|
||||||
|
|
||||||
self.assertUserRedirectedToView(user, 'helpdesk:list')
|
self.assertUserRedirectedToView(user, "helpdesk:list")
|
||||||
|
|
||||||
|
|
||||||
class ReturnToTicketTestCase(TestCase):
|
class ReturnToTicketTestCase(TestCase):
|
||||||
@ -268,13 +280,16 @@ class ReturnToTicketTestCase(TestCase):
|
|||||||
user = get_staff_user()
|
user = get_staff_user()
|
||||||
ticket = create_ticket()
|
ticket = create_ticket()
|
||||||
response = return_to_ticket(user, helpdesk_settings, ticket)
|
response = return_to_ticket(user, helpdesk_settings, ticket)
|
||||||
self.assertEqual(response['location'], ticket.get_absolute_url())
|
self.assertEqual(response["location"], ticket.get_absolute_url())
|
||||||
|
|
||||||
def test_non_staff_user(self):
|
def test_non_staff_user(self):
|
||||||
from helpdesk.views.staff import return_to_ticket
|
from helpdesk.views.staff import return_to_ticket
|
||||||
|
|
||||||
user = User.objects.create_user(
|
user = User.objects.create_user(
|
||||||
username='henry.wensleydale', password='gouda', email='wensleydale@example.com')
|
username="henry.wensleydale",
|
||||||
|
password="gouda",
|
||||||
|
email="wensleydale@example.com",
|
||||||
|
)
|
||||||
ticket = create_ticket()
|
ticket = create_ticket()
|
||||||
response = return_to_ticket(user, helpdesk_settings, ticket)
|
response = return_to_ticket(user, helpdesk_settings, ticket)
|
||||||
self.assertEqual(response['location'], ticket.ticket_url)
|
self.assertEqual(response["location"], ticket.ticket_url)
|
||||||
|
@ -10,7 +10,6 @@ from helpdesk.user import HelpdeskUser
|
|||||||
|
|
||||||
|
|
||||||
class PerQueueStaffMembershipTestCase(TestCase):
|
class PerQueueStaffMembershipTestCase(TestCase):
|
||||||
|
|
||||||
IDENTIFIERS = (1, 2)
|
IDENTIFIERS = (1, 2)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -19,31 +18,33 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
|||||||
and user_2 with access to queue_2 containing 4 tickets
|
and user_2 with access to queue_2 containing 4 tickets
|
||||||
and superuser who should be able to access both queues
|
and superuser who should be able to access both queues
|
||||||
"""
|
"""
|
||||||
self.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION
|
self.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = (
|
||||||
|
settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION
|
||||||
|
)
|
||||||
settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = True
|
settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = True
|
||||||
self.client = Client()
|
self.client = Client()
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
self.superuser = User.objects.create(
|
self.superuser = User.objects.create(
|
||||||
username='superuser',
|
username="superuser",
|
||||||
is_staff=True,
|
is_staff=True,
|
||||||
is_superuser=True,
|
is_superuser=True,
|
||||||
)
|
)
|
||||||
self.superuser.set_password('superuser')
|
self.superuser.set_password("superuser")
|
||||||
self.superuser.save()
|
self.superuser.save()
|
||||||
|
|
||||||
self.identifier_users = {}
|
self.identifier_users = {}
|
||||||
|
|
||||||
for identifier in self.IDENTIFIERS:
|
for identifier in self.IDENTIFIERS:
|
||||||
queue = self.__dict__['queue_%d' % identifier] = Queue.objects.create(
|
queue = self.__dict__["queue_%d" % identifier] = Queue.objects.create(
|
||||||
title='Queue %d' % identifier,
|
title="Queue %d" % identifier,
|
||||||
slug='q%d' % identifier,
|
slug="q%d" % identifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
user = self.__dict__['user_%d' % identifier] = User.objects.create(
|
user = self.__dict__["user_%d" % identifier] = User.objects.create(
|
||||||
username='User_%d' % identifier,
|
username="User_%d" % identifier,
|
||||||
is_staff=True,
|
is_staff=True,
|
||||||
email="foo%s@example.com" % identifier
|
email="foo%s@example.com" % identifier,
|
||||||
)
|
)
|
||||||
user.set_password(str(identifier))
|
user.set_password(str(identifier))
|
||||||
user.save()
|
user.save()
|
||||||
@ -55,13 +56,13 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
|||||||
|
|
||||||
for ticket_number in range(1, identifier + 1):
|
for ticket_number in range(1, identifier + 1):
|
||||||
Ticket.objects.create(
|
Ticket.objects.create(
|
||||||
title='Unassigned Ticket %d in Queue %d' % (
|
title="Unassigned Ticket %d in Queue %d"
|
||||||
ticket_number, identifier),
|
% (ticket_number, identifier),
|
||||||
queue=queue,
|
queue=queue,
|
||||||
)
|
)
|
||||||
Ticket.objects.create(
|
Ticket.objects.create(
|
||||||
title='Ticket %d in Queue %d Assigned to User_%d' % (
|
title="Ticket %d in Queue %d Assigned to User_%d"
|
||||||
ticket_number, identifier, identifier),
|
% (ticket_number, identifier, identifier),
|
||||||
queue=queue,
|
queue=queue,
|
||||||
assigned_to=user,
|
assigned_to=user,
|
||||||
)
|
)
|
||||||
@ -70,7 +71,9 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
|||||||
"""
|
"""
|
||||||
Reset HELPDESK_ENABLE_PER_QUEUE_STAFF_MEMBERSHIP to original value
|
Reset HELPDESK_ENABLE_PER_QUEUE_STAFF_MEMBERSHIP to original value
|
||||||
"""
|
"""
|
||||||
settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = self.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION
|
settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = (
|
||||||
|
self.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION
|
||||||
|
)
|
||||||
|
|
||||||
def test_dashboard_ticket_counts(self):
|
def test_dashboard_ticket_counts(self):
|
||||||
"""
|
"""
|
||||||
@ -81,33 +84,32 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
|||||||
|
|
||||||
# Regular users
|
# Regular users
|
||||||
for identifier in self.IDENTIFIERS:
|
for identifier in self.IDENTIFIERS:
|
||||||
self.client.login(username='User_%d' %
|
self.client.login(username="User_%d" % identifier, password=str(identifier))
|
||||||
identifier, password=str(identifier))
|
response = self.client.get(reverse("helpdesk:dashboard"))
|
||||||
response = self.client.get(reverse('helpdesk:dashboard'))
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(response.context['unassigned_tickets']),
|
len(response.context["unassigned_tickets"]),
|
||||||
identifier,
|
identifier,
|
||||||
'Unassigned tickets were not properly limited by queue membership'
|
"Unassigned tickets were not properly limited by queue membership",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response.context['basic_ticket_stats']['open_ticket_stats'][0][1],
|
response.context["basic_ticket_stats"]["open_ticket_stats"][0][1],
|
||||||
identifier * 2,
|
identifier * 2,
|
||||||
'Basic ticket stats were not properly limited by queue membership'
|
"Basic ticket stats were not properly limited by queue membership",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Superuser
|
# Superuser
|
||||||
self.client.login(username='superuser', password='superuser')
|
self.client.login(username="superuser", password="superuser")
|
||||||
response = self.client.get(reverse('helpdesk:dashboard'))
|
response = self.client.get(reverse("helpdesk:dashboard"))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(response.context['unassigned_tickets']),
|
len(response.context["unassigned_tickets"]),
|
||||||
3,
|
3,
|
||||||
'Unassigned tickets were limited by queue membership for a superuser'
|
"Unassigned tickets were limited by queue membership for a superuser",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response.context['basic_ticket_stats']['open_ticket_stats'][0][1] +
|
response.context["basic_ticket_stats"]["open_ticket_stats"][0][1]
|
||||||
response.context['basic_ticket_stats']['open_ticket_stats'][1][1],
|
+ response.context["basic_ticket_stats"]["open_ticket_stats"][1][1],
|
||||||
6,
|
6,
|
||||||
'Basic ticket stats were limited by queue membership for a superuser'
|
"Basic ticket stats were limited by queue membership for a superuser",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_report_ticket_counts(self):
|
def test_report_ticket_counts(self):
|
||||||
@ -119,44 +121,43 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
|||||||
|
|
||||||
# Regular users
|
# Regular users
|
||||||
for identifier in self.IDENTIFIERS:
|
for identifier in self.IDENTIFIERS:
|
||||||
self.client.login(username='User_%d' %
|
self.client.login(username="User_%d" % identifier, password=str(identifier))
|
||||||
identifier, password=str(identifier))
|
response = self.client.get(reverse("helpdesk:report_index"))
|
||||||
response = self.client.get(reverse('helpdesk:report_index'))
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(response.context['dash_tickets']),
|
len(response.context["dash_tickets"]),
|
||||||
1,
|
1,
|
||||||
'The queues in dash_tickets were not properly limited by queue membership'
|
"The queues in dash_tickets were not properly limited by queue membership",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response.context['dash_tickets'][0]['open'],
|
response.context["dash_tickets"][0]["open"],
|
||||||
identifier * 2,
|
identifier * 2,
|
||||||
'The tickets in dash_tickets were not properly limited by queue membership'
|
"The tickets in dash_tickets were not properly limited by queue membership",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response.context['basic_ticket_stats']['open_ticket_stats'][0][1],
|
response.context["basic_ticket_stats"]["open_ticket_stats"][0][1],
|
||||||
identifier * 2,
|
identifier * 2,
|
||||||
'Basic ticket stats were not properly limited by queue membership'
|
"Basic ticket stats were not properly limited by queue membership",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Superuser
|
# Superuser
|
||||||
self.client.login(username='superuser', password='superuser')
|
self.client.login(username="superuser", password="superuser")
|
||||||
response = self.client.get(reverse('helpdesk:report_index'))
|
response = self.client.get(reverse("helpdesk:report_index"))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(response.context['dash_tickets']),
|
len(response.context["dash_tickets"]),
|
||||||
2,
|
2,
|
||||||
'The queues in dash_tickets were limited by queue membership for a superuser'
|
"The queues in dash_tickets were limited by queue membership for a superuser",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response.context['dash_tickets'][0]['open'] +
|
response.context["dash_tickets"][0]["open"]
|
||||||
response.context['dash_tickets'][1]['open'],
|
+ response.context["dash_tickets"][1]["open"],
|
||||||
6,
|
6,
|
||||||
'The tickets in dash_tickets were limited by queue membership for a superuser'
|
"The tickets in dash_tickets were limited by queue membership for a superuser",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response.context['basic_ticket_stats']['open_ticket_stats'][0][1] +
|
response.context["basic_ticket_stats"]["open_ticket_stats"][0][1]
|
||||||
response.context['basic_ticket_stats']['open_ticket_stats'][1][1],
|
+ response.context["basic_ticket_stats"]["open_ticket_stats"][1][1],
|
||||||
6,
|
6,
|
||||||
'Basic ticket stats were limited by queue membership for a superuser'
|
"Basic ticket stats were limited by queue membership for a superuser",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_ticket_list_per_queue_user_restrictions(self):
|
def test_ticket_list_per_queue_user_restrictions(self):
|
||||||
@ -167,36 +168,38 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
|||||||
"""
|
"""
|
||||||
# Regular users
|
# Regular users
|
||||||
for identifier in self.IDENTIFIERS:
|
for identifier in self.IDENTIFIERS:
|
||||||
self.client.login(username='User_%d' %
|
self.client.login(username="User_%d" % identifier, password=str(identifier))
|
||||||
identifier, password=str(identifier))
|
response = self.client.get(reverse("helpdesk:list"))
|
||||||
response = self.client.get(reverse('helpdesk:list'))
|
tickets = __Query__(
|
||||||
tickets = __Query__(HelpdeskUser(
|
HelpdeskUser(self.identifier_users[identifier]),
|
||||||
self.identifier_users[identifier]), base64query=response.context['urlsafe_query']).get()
|
base64query=response.context["urlsafe_query"],
|
||||||
|
).get()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(tickets),
|
len(tickets),
|
||||||
identifier * 2,
|
identifier * 2,
|
||||||
'Ticket list was not properly limited by queue membership'
|
"Ticket list was not properly limited by queue membership",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(response.context['queue_choices']),
|
len(response.context["queue_choices"]),
|
||||||
1,
|
1,
|
||||||
'Queue choices were not properly limited by queue membership'
|
"Queue choices were not properly limited by queue membership",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response.context['queue_choices'][0],
|
response.context["queue_choices"][0],
|
||||||
Queue.objects.get(title="Queue %d" % identifier),
|
Queue.objects.get(title="Queue %d" % identifier),
|
||||||
'Queue choices were not properly limited by queue membership'
|
"Queue choices were not properly limited by queue membership",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Superuser
|
# Superuser
|
||||||
self.client.login(username='superuser', password='superuser')
|
self.client.login(username="superuser", password="superuser")
|
||||||
response = self.client.get(reverse('helpdesk:list'))
|
response = self.client.get(reverse("helpdesk:list"))
|
||||||
tickets = __Query__(HelpdeskUser(self.superuser),
|
tickets = __Query__(
|
||||||
base64query=response.context['urlsafe_query']).get()
|
HelpdeskUser(self.superuser), base64query=response.context["urlsafe_query"]
|
||||||
|
).get()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(tickets),
|
len(tickets),
|
||||||
6,
|
6,
|
||||||
'Ticket list was limited by queue membership for a superuser'
|
"Ticket list was limited by queue membership for a superuser",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_ticket_reports_per_queue_user_restrictions(self):
|
def test_ticket_reports_per_queue_user_restrictions(self):
|
||||||
@ -207,61 +210,60 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
|||||||
"""
|
"""
|
||||||
# Regular users
|
# Regular users
|
||||||
for identifier in self.IDENTIFIERS:
|
for identifier in self.IDENTIFIERS:
|
||||||
self.client.login(username='User_%d' %
|
self.client.login(username="User_%d" % identifier, password=str(identifier))
|
||||||
identifier, password=str(identifier))
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse('helpdesk:run_report', kwargs={'report': 'userqueue'})
|
reverse("helpdesk:run_report", kwargs={"report": "userqueue"})
|
||||||
)
|
)
|
||||||
# Only two columns of data should be present: ticket counts for
|
# Only two columns of data should be present: ticket counts for
|
||||||
# unassigned and this user only
|
# unassigned and this user only
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(response.context['data']),
|
len(response.context["data"]),
|
||||||
2,
|
2,
|
||||||
'Queues in report were not properly limited by queue membership'
|
"Queues in report were not properly limited by queue membership",
|
||||||
)
|
)
|
||||||
# Each user should see a total number of tickets equal to twice
|
# Each user should see a total number of tickets equal to twice
|
||||||
# their ID
|
# their ID
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sum([sum(user_tickets[1:])
|
sum(
|
||||||
for user_tickets in response.context['data']]),
|
[sum(user_tickets[1:]) for user_tickets in response.context["data"]]
|
||||||
|
),
|
||||||
identifier * 2,
|
identifier * 2,
|
||||||
'Tickets in report were not properly limited by queue membership'
|
"Tickets in report were not properly limited by queue membership",
|
||||||
)
|
)
|
||||||
# Each user should only be able to pick 1 queue
|
# Each user should only be able to pick 1 queue
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(response.context['headings']),
|
len(response.context["headings"]),
|
||||||
2,
|
2,
|
||||||
'Queue choices were not properly limited by queue membership'
|
"Queue choices were not properly limited by queue membership",
|
||||||
)
|
)
|
||||||
# The queue each user can pick should be the queue named after
|
# The queue each user can pick should be the queue named after
|
||||||
# their ID
|
# their ID
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response.context['headings'][1],
|
response.context["headings"][1],
|
||||||
"Queue %d" % identifier,
|
"Queue %d" % identifier,
|
||||||
'Queue choices were not properly limited by queue membership'
|
"Queue choices were not properly limited by queue membership",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Superuser
|
# Superuser
|
||||||
self.client.login(username='superuser', password='superuser')
|
self.client.login(username="superuser", password="superuser")
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse('helpdesk:run_report', kwargs={'report': 'userqueue'})
|
reverse("helpdesk:run_report", kwargs={"report": "userqueue"})
|
||||||
)
|
)
|
||||||
# Superuser should see ticket counts for all two queues, which includes
|
# Superuser should see ticket counts for all two queues, which includes
|
||||||
# three columns: unassigned and both user 1 and user 2
|
# three columns: unassigned and both user 1 and user 2
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(response.context['data'][0]),
|
len(response.context["data"][0]),
|
||||||
3,
|
3,
|
||||||
'Queues in report were improperly limited by queue membership for a superuser'
|
"Queues in report were improperly limited by queue membership for a superuser",
|
||||||
)
|
)
|
||||||
# Superuser should see the total ticket count of three tickets
|
# Superuser should see the total ticket count of three tickets
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sum([sum(user_tickets[1:])
|
sum([sum(user_tickets[1:]) for user_tickets in response.context["data"]]),
|
||||||
for user_tickets in response.context['data']]),
|
|
||||||
6,
|
6,
|
||||||
'Tickets in report were improperly limited by queue membership for a superuser'
|
"Tickets in report were improperly limited by queue membership for a superuser",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(response.context['headings']),
|
len(response.context["headings"]),
|
||||||
3,
|
3,
|
||||||
'Queue choices were improperly limited by queue membership for a superuser'
|
"Queue choices were improperly limited by queue membership for a superuser",
|
||||||
)
|
)
|
||||||
|
@ -16,39 +16,51 @@ class PublicActionsTestCase(TestCase):
|
|||||||
"""
|
"""
|
||||||
Create a queue & ticket we can use for later tests.
|
Create a queue & ticket we can use for later tests.
|
||||||
"""
|
"""
|
||||||
self.queue = Queue.objects.create(title='Queue 1',
|
self.queue = Queue.objects.create(
|
||||||
slug='q',
|
title="Queue 1",
|
||||||
allow_public_submission=True,
|
slug="q",
|
||||||
new_ticket_cc='new.public@example.com',
|
allow_public_submission=True,
|
||||||
updated_ticket_cc='update.public@example.com')
|
new_ticket_cc="new.public@example.com",
|
||||||
self.ticket = Ticket.objects.create(title='Test Ticket',
|
updated_ticket_cc="update.public@example.com",
|
||||||
queue=self.queue,
|
)
|
||||||
submitter_email='test.submitter@example.com',
|
self.ticket = Ticket.objects.create(
|
||||||
description='This is a test ticket.')
|
title="Test Ticket",
|
||||||
|
queue=self.queue,
|
||||||
|
submitter_email="test.submitter@example.com",
|
||||||
|
description="This is a test ticket.",
|
||||||
|
)
|
||||||
|
|
||||||
self.client = Client()
|
self.client = Client()
|
||||||
|
|
||||||
def test_public_view_ticket(self):
|
def test_public_view_ticket(self):
|
||||||
# Without key, we get 403
|
# Without key, we get 403
|
||||||
response = self.client.get('%s?ticket=%s&email=%s' % (
|
response = self.client.get(
|
||||||
reverse('helpdesk:public_view'),
|
"%s?ticket=%s&email=%s"
|
||||||
self.ticket.ticket_for_url,
|
% (
|
||||||
'test.submitter@example.com'))
|
reverse("helpdesk:public_view"),
|
||||||
|
self.ticket.ticket_for_url,
|
||||||
|
"test.submitter@example.com",
|
||||||
|
)
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html')
|
self.assertTemplateNotUsed(response, "helpdesk/public_view_form.html")
|
||||||
# With a key it works
|
# With a key it works
|
||||||
response = self.client.get('%s?ticket=%s&email=%s&key=%s' % (
|
response = self.client.get(
|
||||||
reverse('helpdesk:public_view'),
|
"%s?ticket=%s&email=%s&key=%s"
|
||||||
self.ticket.ticket_for_url,
|
% (
|
||||||
'test.submitter@example.com',
|
reverse("helpdesk:public_view"),
|
||||||
self.ticket.secret_key))
|
self.ticket.ticket_for_url,
|
||||||
|
"test.submitter@example.com",
|
||||||
|
self.ticket.secret_key,
|
||||||
|
)
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTemplateUsed(response, 'helpdesk/public_view_ticket.html')
|
self.assertTemplateUsed(response, "helpdesk/public_view_ticket.html")
|
||||||
|
|
||||||
def test_public_close(self):
|
def test_public_close(self):
|
||||||
old_status = self.ticket.status
|
old_status = self.ticket.status
|
||||||
old_resolution = self.ticket.resolution
|
old_resolution = self.ticket.resolution
|
||||||
resolution_text = 'Resolved by test script'
|
resolution_text = "Resolved by test script"
|
||||||
|
|
||||||
ticket = Ticket.objects.get(id=self.ticket.id)
|
ticket = Ticket.objects.get(id=self.ticket.id)
|
||||||
|
|
||||||
@ -58,20 +70,23 @@ class PublicActionsTestCase(TestCase):
|
|||||||
|
|
||||||
current_followups = ticket.followup_set.all().count()
|
current_followups = ticket.followup_set.all().count()
|
||||||
|
|
||||||
response = self.client.get('%s?ticket=%s&email=%s&close&key=%s' % (
|
response = self.client.get(
|
||||||
reverse('helpdesk:public_view'),
|
"%s?ticket=%s&email=%s&close&key=%s"
|
||||||
ticket.ticket_for_url,
|
% (
|
||||||
'test.submitter@example.com',
|
reverse("helpdesk:public_view"),
|
||||||
ticket.secret_key))
|
ticket.ticket_for_url,
|
||||||
|
"test.submitter@example.com",
|
||||||
|
ticket.secret_key,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
ticket = Ticket.objects.get(id=self.ticket.id)
|
ticket = Ticket.objects.get(id=self.ticket.id)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html')
|
self.assertTemplateNotUsed(response, "helpdesk/public_view_form.html")
|
||||||
self.assertEqual(ticket.status, Ticket.CLOSED_STATUS)
|
self.assertEqual(ticket.status, Ticket.CLOSED_STATUS)
|
||||||
self.assertEqual(ticket.resolution, resolution_text)
|
self.assertEqual(ticket.resolution, resolution_text)
|
||||||
self.assertEqual(current_followups + 1,
|
self.assertEqual(current_followups + 1, ticket.followup_set.all().count())
|
||||||
ticket.followup_set.all().count())
|
|
||||||
|
|
||||||
ticket.resolution = old_resolution
|
ticket.resolution = old_resolution
|
||||||
ticket.status = old_status
|
ticket.status = old_status
|
||||||
|
@ -47,25 +47,57 @@ class QueryTests(TestCase):
|
|||||||
"""Create a staff user and login"""
|
"""Create a staff user and login"""
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
self.user = User.objects.create(
|
self.user = User.objects.create(
|
||||||
username='User_1',
|
username="User_1",
|
||||||
is_staff=is_staff,
|
is_staff=is_staff,
|
||||||
)
|
)
|
||||||
self.user.set_password('pass')
|
self.user.set_password("pass")
|
||||||
self.user.save()
|
self.user.save()
|
||||||
self.client.login(username='User_1', password='pass')
|
self.client.login(username="User_1", password="pass")
|
||||||
|
|
||||||
def test_query_basic(self):
|
def test_query_basic(self):
|
||||||
self.loginUser()
|
self.loginUser()
|
||||||
query = query_to_base64({})
|
query = query_to_base64({})
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse('helpdesk:datatables_ticket_list', args=[query]))
|
reverse("helpdesk:datatables_ticket_list", args=[query])
|
||||||
|
)
|
||||||
resp_json = response.json()
|
resp_json = response.json()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
resp_json,
|
resp_json,
|
||||||
{
|
{
|
||||||
"data":
|
"data": [
|
||||||
[{"ticket": "1 [test_queue-1]", "id": 1, "priority": 3, "title": "unassigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": ""},
|
{
|
||||||
{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": resp_json["data"][1]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
|
"ticket": "1 [test_queue-1]",
|
||||||
|
"id": 1,
|
||||||
|
"priority": 3,
|
||||||
|
"title": "unassigned to kbitem",
|
||||||
|
"queue": {"title": "Test queue", "id": 1},
|
||||||
|
"status": "Open",
|
||||||
|
"created": resp_json["data"][0]["created"],
|
||||||
|
"due_date": None,
|
||||||
|
"assigned_to": "None",
|
||||||
|
"submitter": None,
|
||||||
|
"last_followup": None,
|
||||||
|
"row_class": "",
|
||||||
|
"time_spent": "",
|
||||||
|
"kbitem": "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket": "2 [test_queue-2]",
|
||||||
|
"id": 2,
|
||||||
|
"priority": 3,
|
||||||
|
"title": "assigned to kbitem",
|
||||||
|
"queue": {"title": "Test queue", "id": 1},
|
||||||
|
"status": "Open",
|
||||||
|
"created": resp_json["data"][1]["created"],
|
||||||
|
"due_date": None,
|
||||||
|
"assigned_to": "None",
|
||||||
|
"submitter": None,
|
||||||
|
"last_followup": None,
|
||||||
|
"row_class": "",
|
||||||
|
"time_spent": "",
|
||||||
|
"kbitem": "KBItem 1",
|
||||||
|
},
|
||||||
|
],
|
||||||
"recordsFiltered": 2,
|
"recordsFiltered": 2,
|
||||||
"recordsTotal": 2,
|
"recordsTotal": 2,
|
||||||
"draw": 0,
|
"draw": 0,
|
||||||
@ -74,18 +106,32 @@ class QueryTests(TestCase):
|
|||||||
|
|
||||||
def test_query_by_kbitem(self):
|
def test_query_by_kbitem(self):
|
||||||
self.loginUser()
|
self.loginUser()
|
||||||
query = query_to_base64(
|
query = query_to_base64({"filtering": {"kbitem__in": [self.kbitem1.pk]}})
|
||||||
{'filtering': {'kbitem__in': [self.kbitem1.pk]}}
|
|
||||||
)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse('helpdesk:datatables_ticket_list', args=[query]))
|
reverse("helpdesk:datatables_ticket_list", args=[query])
|
||||||
|
)
|
||||||
resp_json = response.json()
|
resp_json = response.json()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
resp_json,
|
resp_json,
|
||||||
{
|
{
|
||||||
"data":
|
"data": [
|
||||||
[{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open",
|
{
|
||||||
"created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
|
"ticket": "2 [test_queue-2]",
|
||||||
|
"id": 2,
|
||||||
|
"priority": 3,
|
||||||
|
"title": "assigned to kbitem",
|
||||||
|
"queue": {"title": "Test queue", "id": 1},
|
||||||
|
"status": "Open",
|
||||||
|
"created": resp_json["data"][0]["created"],
|
||||||
|
"due_date": None,
|
||||||
|
"assigned_to": "None",
|
||||||
|
"submitter": None,
|
||||||
|
"last_followup": None,
|
||||||
|
"row_class": "",
|
||||||
|
"time_spent": "",
|
||||||
|
"kbitem": "KBItem 1",
|
||||||
|
}
|
||||||
|
],
|
||||||
"recordsFiltered": 1,
|
"recordsFiltered": 1,
|
||||||
"recordsTotal": 1,
|
"recordsTotal": 1,
|
||||||
"draw": 0,
|
"draw": 0,
|
||||||
@ -94,18 +140,32 @@ class QueryTests(TestCase):
|
|||||||
|
|
||||||
def test_query_by_no_kbitem(self):
|
def test_query_by_no_kbitem(self):
|
||||||
self.loginUser()
|
self.loginUser()
|
||||||
query = query_to_base64(
|
query = query_to_base64({"filtering_null": {"kbitem__isnull": True}})
|
||||||
{'filtering_null': {'kbitem__isnull': True}}
|
|
||||||
)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse('helpdesk:datatables_ticket_list', args=[query]))
|
reverse("helpdesk:datatables_ticket_list", args=[query])
|
||||||
|
)
|
||||||
resp_json = response.json()
|
resp_json = response.json()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
resp_json,
|
resp_json,
|
||||||
{
|
{
|
||||||
"data":
|
"data": [
|
||||||
[{"ticket": "1 [test_queue-1]", "id": 1, "priority": 3, "title": "unassigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open",
|
{
|
||||||
"created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": ""}],
|
"ticket": "1 [test_queue-1]",
|
||||||
|
"id": 1,
|
||||||
|
"priority": 3,
|
||||||
|
"title": "unassigned to kbitem",
|
||||||
|
"queue": {"title": "Test queue", "id": 1},
|
||||||
|
"status": "Open",
|
||||||
|
"created": resp_json["data"][0]["created"],
|
||||||
|
"due_date": None,
|
||||||
|
"assigned_to": "None",
|
||||||
|
"submitter": None,
|
||||||
|
"last_followup": None,
|
||||||
|
"row_class": "",
|
||||||
|
"time_spent": "",
|
||||||
|
"kbitem": "",
|
||||||
|
}
|
||||||
|
],
|
||||||
"recordsFiltered": 1,
|
"recordsFiltered": 1,
|
||||||
"recordsTotal": 1,
|
"recordsTotal": 1,
|
||||||
"draw": 0,
|
"draw": 0,
|
||||||
|
@ -7,24 +7,25 @@ from helpdesk.tests.helpers import get_user
|
|||||||
|
|
||||||
class TestSavingSharedQuery(TestCase):
|
class TestSavingSharedQuery(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
q = Queue(title='Q1', slug='q1')
|
q = Queue(title="Q1", slug="q1")
|
||||||
q.save()
|
q.save()
|
||||||
self.q = q
|
self.q = q
|
||||||
|
|
||||||
def test_cansavequery(self):
|
def test_cansavequery(self):
|
||||||
"""Can a query be saved"""
|
"""Can a query be saved"""
|
||||||
url = reverse('helpdesk:savequery')
|
url = reverse("helpdesk:savequery")
|
||||||
self.client.login(username=get_user(is_staff=True).get_username(),
|
self.client.login(
|
||||||
password='password')
|
username=get_user(is_staff=True).get_username(), password="password"
|
||||||
|
)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
url,
|
url,
|
||||||
data={
|
data={
|
||||||
'title': 'ticket on my queue',
|
"title": "ticket on my queue",
|
||||||
'queue': self.q,
|
"queue": self.q,
|
||||||
'shared': 'on',
|
"shared": "on",
|
||||||
'query_encoded':
|
"query_encoded": "KGRwMApWZmlsdGVyaW5nCnAxCihkcDIKVnN0YXR1c19faW4KcDMKKG"
|
||||||
'KGRwMApWZmlsdGVyaW5nCnAxCihkcDIKVnN0YXR1c19faW4KcDMKKG'
|
"xwNApJMQphSTIKYUkzCmFzc1Zzb3J0aW5nCnA1ClZjcmVhdGVkCnA2CnMu",
|
||||||
'xwNApJMQphSTIKYUkzCmFzc1Zzb3J0aW5nCnA1ClZjcmVhdGVkCnA2CnMu'
|
},
|
||||||
})
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertTrue('tickets/?saved_query=1' in response.url)
|
self.assertTrue("tickets/?saved_query=1" in response.url)
|
||||||
|
@ -17,29 +17,29 @@ except ImportError: # python 2
|
|||||||
|
|
||||||
|
|
||||||
class TicketActionsTestCase(TestCase):
|
class TicketActionsTestCase(TestCase):
|
||||||
fixtures = ['emailtemplate.json']
|
fixtures = ["emailtemplate.json"]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.queue_public = Queue.objects.create(
|
self.queue_public = Queue.objects.create(
|
||||||
title='Queue 1',
|
title="Queue 1",
|
||||||
slug='q1',
|
slug="q1",
|
||||||
allow_public_submission=True,
|
allow_public_submission=True,
|
||||||
new_ticket_cc='new.public@example.com',
|
new_ticket_cc="new.public@example.com",
|
||||||
updated_ticket_cc='update.public@example.com'
|
updated_ticket_cc="update.public@example.com",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.queue_private = Queue.objects.create(
|
self.queue_private = Queue.objects.create(
|
||||||
title='Queue 2',
|
title="Queue 2",
|
||||||
slug='q2',
|
slug="q2",
|
||||||
allow_public_submission=False,
|
allow_public_submission=False,
|
||||||
new_ticket_cc='new.private@example.com',
|
new_ticket_cc="new.private@example.com",
|
||||||
updated_ticket_cc='update.private@example.com'
|
updated_ticket_cc="update.private@example.com",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.ticket_data = {
|
self.ticket_data = {
|
||||||
'queue': self.queue_public,
|
"queue": self.queue_public,
|
||||||
'title': 'Test Ticket',
|
"title": "Test Ticket",
|
||||||
'description': 'Some Test Ticket',
|
"description": "Some Test Ticket",
|
||||||
}
|
}
|
||||||
|
|
||||||
self.client = Client()
|
self.client = Client()
|
||||||
@ -49,24 +49,22 @@ class TicketActionsTestCase(TestCase):
|
|||||||
"""Create a staff user and login"""
|
"""Create a staff user and login"""
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
self.user = User.objects.create(
|
self.user = User.objects.create(
|
||||||
username='User_1',
|
username="User_1",
|
||||||
is_staff=is_staff,
|
is_staff=is_staff,
|
||||||
)
|
)
|
||||||
self.user.set_password('pass')
|
self.user.set_password("pass")
|
||||||
self.user.save()
|
self.user.save()
|
||||||
self.client.login(username='User_1', password='pass')
|
self.client.login(username="User_1", password="pass")
|
||||||
|
|
||||||
def test_ticket_markdown(self):
|
def test_ticket_markdown(self):
|
||||||
|
|
||||||
ticket_data = {
|
ticket_data = {
|
||||||
'queue': self.queue_public,
|
"queue": self.queue_public,
|
||||||
'title': 'Test Ticket',
|
"title": "Test Ticket",
|
||||||
'description': '*bold*',
|
"description": "*bold*",
|
||||||
}
|
}
|
||||||
|
|
||||||
ticket = Ticket.objects.create(**ticket_data)
|
ticket = Ticket.objects.create(**ticket_data)
|
||||||
self.assertEqual(ticket.get_markdown(),
|
self.assertEqual(ticket.get_markdown(), "<p><em>bold</em></p>")
|
||||||
"<p><em>bold</em></p>")
|
|
||||||
|
|
||||||
def test_delete_ticket_staff(self):
|
def test_delete_ticket_staff(self):
|
||||||
# make staff user
|
# make staff user
|
||||||
@ -76,13 +74,14 @@ class TicketActionsTestCase(TestCase):
|
|||||||
ticket = Ticket.objects.create(**self.ticket_data)
|
ticket = Ticket.objects.create(**self.ticket_data)
|
||||||
ticket_id = ticket.id
|
ticket_id = ticket.id
|
||||||
|
|
||||||
response = self.client.get(reverse('helpdesk:delete', kwargs={
|
response = self.client.get(
|
||||||
'ticket_id': ticket_id}), follow=True)
|
reverse("helpdesk:delete", kwargs={"ticket_id": ticket_id}), follow=True
|
||||||
self.assertContains(
|
)
|
||||||
response, 'Are you sure you want to delete this ticket')
|
self.assertContains(response, "Are you sure you want to delete this ticket")
|
||||||
|
|
||||||
response = self.client.post(reverse('helpdesk:delete', kwargs={
|
response = self.client.post(
|
||||||
'ticket_id': ticket_id}), follow=True)
|
reverse("helpdesk:delete", kwargs={"ticket_id": ticket_id}), follow=True
|
||||||
|
)
|
||||||
first_redirect = response.redirect_chain[0]
|
first_redirect = response.redirect_chain[0]
|
||||||
first_redirect_url = first_redirect[0]
|
first_redirect_url = first_redirect[0]
|
||||||
|
|
||||||
@ -90,7 +89,7 @@ class TicketActionsTestCase(TestCase):
|
|||||||
# Django 1.9 compatible way of testing this
|
# Django 1.9 compatible way of testing this
|
||||||
# https://docs.djangoproject.com/en/1.9/releases/1.9/#http-redirects-no-longer-forced-to-absolute-uris
|
# https://docs.djangoproject.com/en/1.9/releases/1.9/#http-redirects-no-longer-forced-to-absolute-uris
|
||||||
urlparts = urlparse(first_redirect_url)
|
urlparts = urlparse(first_redirect_url)
|
||||||
self.assertEqual(urlparts.path, reverse('helpdesk:home'))
|
self.assertEqual(urlparts.path, reverse("helpdesk:home"))
|
||||||
|
|
||||||
# test ticket deleted
|
# test ticket deleted
|
||||||
with self.assertRaises(Ticket.DoesNotExist):
|
with self.assertRaises(Ticket.DoesNotExist):
|
||||||
@ -105,15 +104,15 @@ class TicketActionsTestCase(TestCase):
|
|||||||
# create second user
|
# create second user
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
self.user2 = User.objects.create(
|
self.user2 = User.objects.create(
|
||||||
username='User_2',
|
username="User_2",
|
||||||
is_staff=True,
|
is_staff=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
initial_data = {
|
initial_data = {
|
||||||
'title': 'Private ticket test',
|
"title": "Private ticket test",
|
||||||
'queue': self.queue_public,
|
"queue": self.queue_public,
|
||||||
'assigned_to': self.user,
|
"assigned_to": self.user,
|
||||||
'status': Ticket.OPEN_STATUS,
|
"status": Ticket.OPEN_STATUS,
|
||||||
}
|
}
|
||||||
|
|
||||||
# create ticket
|
# create ticket
|
||||||
@ -122,39 +121,45 @@ class TicketActionsTestCase(TestCase):
|
|||||||
|
|
||||||
# assign new owner
|
# assign new owner
|
||||||
post_data = {
|
post_data = {
|
||||||
'owner': self.user2.id,
|
"owner": self.user2.id,
|
||||||
}
|
}
|
||||||
response = self.client.post(reverse('helpdesk:update', kwargs={
|
response = self.client.post(
|
||||||
'ticket_id': ticket_id}), post_data, follow=True)
|
reverse("helpdesk:update", kwargs={"ticket_id": ticket_id}),
|
||||||
self.assertContains(response, 'Changed Owner from User_1 to User_2')
|
post_data,
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
self.assertContains(response, "Changed Owner from User_1 to User_2")
|
||||||
|
|
||||||
# change status with users email assigned and submitter email assigned,
|
# change status with users email assigned and submitter email assigned,
|
||||||
# which triggers emails being sent
|
# which triggers emails being sent
|
||||||
ticket.assigned_to = self.user2
|
ticket.assigned_to = self.user2
|
||||||
ticket.submitter_email = 'submitter@test.com'
|
ticket.submitter_email = "submitter@test.com"
|
||||||
ticket.save()
|
ticket.save()
|
||||||
self.user2.email = 'user2@test.com'
|
self.user2.email = "user2@test.com"
|
||||||
self.user2.save()
|
self.user2.save()
|
||||||
self.user.email = 'user1@test.com'
|
self.user.email = "user1@test.com"
|
||||||
self.user.save()
|
self.user.save()
|
||||||
post_data = {
|
post_data = {"new_status": Ticket.CLOSED_STATUS, "public": True}
|
||||||
'new_status': Ticket.CLOSED_STATUS,
|
|
||||||
'public': True
|
|
||||||
}
|
|
||||||
|
|
||||||
# do this also to a newly assigned user (different from logged in one)
|
# do this also to a newly assigned user (different from logged in one)
|
||||||
ticket.assigned_to = self.user
|
ticket.assigned_to = self.user
|
||||||
response = self.client.post(reverse('helpdesk:update', kwargs={
|
response = self.client.post(
|
||||||
'ticket_id': ticket_id}), post_data, follow=True)
|
reverse("helpdesk:update", kwargs={"ticket_id": ticket_id}),
|
||||||
self.assertContains(response, 'Changed Status from Open to Closed')
|
post_data,
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
self.assertContains(response, "Changed Status from Open to Closed")
|
||||||
post_data = {
|
post_data = {
|
||||||
'new_status': Ticket.OPEN_STATUS,
|
"new_status": Ticket.OPEN_STATUS,
|
||||||
'owner': self.user2.id,
|
"owner": self.user2.id,
|
||||||
'public': True
|
"public": True,
|
||||||
}
|
}
|
||||||
response = self.client.post(reverse('helpdesk:update', kwargs={
|
response = self.client.post(
|
||||||
'ticket_id': ticket_id}), post_data, follow=True)
|
reverse("helpdesk:update", kwargs={"ticket_id": ticket_id}),
|
||||||
self.assertContains(response, 'Changed Status from Open to Closed')
|
post_data,
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
self.assertContains(response, "Changed Status from Open to Closed")
|
||||||
|
|
||||||
def test_can_access_ticket(self):
|
def test_can_access_ticket(self):
|
||||||
"""Tests whether non-staff but assigned user still counts as owner"""
|
"""Tests whether non-staff but assigned user still counts as owner"""
|
||||||
@ -165,24 +170,22 @@ class TicketActionsTestCase(TestCase):
|
|||||||
# create second user
|
# create second user
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
self.user2 = User.objects.create(
|
self.user2 = User.objects.create(
|
||||||
username='User_2',
|
username="User_2",
|
||||||
is_staff=False,
|
is_staff=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
initial_data = {
|
initial_data = {
|
||||||
'title': 'Private ticket test',
|
"title": "Private ticket test",
|
||||||
'queue': self.queue_private,
|
"queue": self.queue_private,
|
||||||
'assigned_to': self.user,
|
"assigned_to": self.user,
|
||||||
'status': Ticket.OPEN_STATUS,
|
"status": Ticket.OPEN_STATUS,
|
||||||
}
|
}
|
||||||
|
|
||||||
# create ticket
|
# create ticket
|
||||||
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = True
|
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = True
|
||||||
ticket = Ticket.objects.create(**initial_data)
|
ticket = Ticket.objects.create(**initial_data)
|
||||||
self.assertEqual(HelpdeskUser(
|
self.assertEqual(HelpdeskUser(self.user).can_access_ticket(ticket), True)
|
||||||
self.user).can_access_ticket(ticket), True)
|
self.assertEqual(HelpdeskUser(self.user2).can_access_ticket(ticket), False)
|
||||||
self.assertEqual(HelpdeskUser(
|
|
||||||
self.user2).can_access_ticket(ticket), False)
|
|
||||||
|
|
||||||
def test_num_to_link(self):
|
def test_num_to_link(self):
|
||||||
"""Test that we are correctly expanding links to tickets from IDs"""
|
"""Test that we are correctly expanding links to tickets from IDs"""
|
||||||
@ -191,10 +194,10 @@ class TicketActionsTestCase(TestCase):
|
|||||||
self.loginUser()
|
self.loginUser()
|
||||||
|
|
||||||
initial_data = {
|
initial_data = {
|
||||||
'title': 'Some private ticket',
|
"title": "Some private ticket",
|
||||||
'queue': self.queue_public,
|
"queue": self.queue_public,
|
||||||
'assigned_to': self.user,
|
"assigned_to": self.user,
|
||||||
'status': Ticket.OPEN_STATUS,
|
"status": Ticket.OPEN_STATUS,
|
||||||
}
|
}
|
||||||
|
|
||||||
# create ticket
|
# create ticket
|
||||||
@ -202,18 +205,23 @@ class TicketActionsTestCase(TestCase):
|
|||||||
ticket_id = ticket.id
|
ticket_id = ticket.id
|
||||||
|
|
||||||
# generate the URL text
|
# generate the URL text
|
||||||
result = num_to_link('this is ticket#%s' % ticket_id)
|
result = num_to_link("this is ticket#%s" % ticket_id)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
result, "this is ticket <a href='/tickets/%s/' class='ticket_link_status ticket_link_status_Open'>#%s</a>" % (ticket_id, ticket_id))
|
result,
|
||||||
|
"this is ticket <a href='/tickets/%s/' class='ticket_link_status ticket_link_status_Open'>#%s</a>"
|
||||||
|
% (ticket_id, ticket_id),
|
||||||
|
)
|
||||||
|
|
||||||
result2 = num_to_link(
|
result2 = num_to_link("whoa another ticket is here #%s huh" % ticket_id)
|
||||||
'whoa another ticket is here #%s huh' % ticket_id)
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
result2, "whoa another ticket is here <a href='/tickets/%s/' class='ticket_link_status ticket_link_status_Open'>#%s</a> huh" % (ticket_id, ticket_id))
|
result2,
|
||||||
|
"whoa another ticket is here <a href='/tickets/%s/' class='ticket_link_status ticket_link_status_Open'>#%s</a> huh"
|
||||||
|
% (ticket_id, ticket_id),
|
||||||
|
)
|
||||||
|
|
||||||
def test_create_ticket_getform(self):
|
def test_create_ticket_getform(self):
|
||||||
self.loginUser()
|
self.loginUser()
|
||||||
response = self.client.get(reverse('helpdesk:submit'), follow=True)
|
response = self.client.get(reverse("helpdesk:submit"), follow=True)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# TODO this needs to be checked further
|
# TODO this needs to be checked further
|
||||||
@ -224,61 +232,62 @@ class TicketActionsTestCase(TestCase):
|
|||||||
# Create two tickets
|
# Create two tickets
|
||||||
ticket_1 = Ticket.objects.create(
|
ticket_1 = Ticket.objects.create(
|
||||||
queue=self.queue_public,
|
queue=self.queue_public,
|
||||||
title='Ticket 1',
|
title="Ticket 1",
|
||||||
description='Description from ticket 1',
|
description="Description from ticket 1",
|
||||||
submitter_email='user1@mail.com',
|
submitter_email="user1@mail.com",
|
||||||
status=Ticket.RESOLVED_STATUS,
|
status=Ticket.RESOLVED_STATUS,
|
||||||
resolution='Awesome resolution for ticket 1'
|
resolution="Awesome resolution for ticket 1",
|
||||||
)
|
)
|
||||||
ticket_1_follow_up = ticket_1.followup_set.create(
|
ticket_1_follow_up = ticket_1.followup_set.create(title="Ticket 1 creation")
|
||||||
title='Ticket 1 creation')
|
|
||||||
ticket_1_cc = ticket_1.ticketcc_set.create(user=self.user)
|
ticket_1_cc = ticket_1.ticketcc_set.create(user=self.user)
|
||||||
ticket_1_created = ticket_1.created
|
ticket_1_created = ticket_1.created
|
||||||
due_date = timezone.now()
|
due_date = timezone.now()
|
||||||
ticket_2 = Ticket.objects.create(
|
ticket_2 = Ticket.objects.create(
|
||||||
queue=self.queue_public,
|
queue=self.queue_public,
|
||||||
title='Ticket 2',
|
title="Ticket 2",
|
||||||
description='Description from ticket 2',
|
description="Description from ticket 2",
|
||||||
submitter_email='user2@mail.com',
|
submitter_email="user2@mail.com",
|
||||||
due_date=due_date,
|
due_date=due_date,
|
||||||
assigned_to=self.user
|
assigned_to=self.user,
|
||||||
)
|
)
|
||||||
ticket_2_follow_up = ticket_1.followup_set.create(
|
ticket_2_follow_up = ticket_1.followup_set.create(title="Ticket 2 creation")
|
||||||
title='Ticket 2 creation')
|
ticket_2_cc = ticket_2.ticketcc_set.create(email="random@mail.com")
|
||||||
ticket_2_cc = ticket_2.ticketcc_set.create(email='random@mail.com')
|
|
||||||
|
|
||||||
# Create custom fields and set values for tickets
|
# Create custom fields and set values for tickets
|
||||||
custom_field_1 = CustomField.objects.create(
|
custom_field_1 = CustomField.objects.create(
|
||||||
name='test',
|
name="test",
|
||||||
label='Test',
|
label="Test",
|
||||||
data_type='varchar',
|
data_type="varchar",
|
||||||
)
|
)
|
||||||
ticket_1_field_1 = 'This is for the test field'
|
ticket_1_field_1 = "This is for the test field"
|
||||||
ticket_1.ticketcustomfieldvalue_set.create(
|
ticket_1.ticketcustomfieldvalue_set.create(
|
||||||
field=custom_field_1, value=ticket_1_field_1)
|
field=custom_field_1, value=ticket_1_field_1
|
||||||
ticket_2_field_1 = 'Another test text'
|
|
||||||
ticket_2.ticketcustomfieldvalue_set.create(
|
|
||||||
field=custom_field_1, value=ticket_2_field_1)
|
|
||||||
custom_field_2 = CustomField.objects.create(
|
|
||||||
name='number',
|
|
||||||
label='Number',
|
|
||||||
data_type='integer',
|
|
||||||
)
|
)
|
||||||
ticket_2_field_2 = '444'
|
ticket_2_field_1 = "Another test text"
|
||||||
ticket_2.ticketcustomfieldvalue_set.create(
|
ticket_2.ticketcustomfieldvalue_set.create(
|
||||||
field=custom_field_2, value=ticket_2_field_2)
|
field=custom_field_1, value=ticket_2_field_1
|
||||||
|
)
|
||||||
|
custom_field_2 = CustomField.objects.create(
|
||||||
|
name="number",
|
||||||
|
label="Number",
|
||||||
|
data_type="integer",
|
||||||
|
)
|
||||||
|
ticket_2_field_2 = "444"
|
||||||
|
ticket_2.ticketcustomfieldvalue_set.create(
|
||||||
|
field=custom_field_2, value=ticket_2_field_2
|
||||||
|
)
|
||||||
|
|
||||||
# Check that it correctly redirects to the intermediate page
|
# Check that it correctly redirects to the intermediate page
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse('helpdesk:mass_update'),
|
reverse("helpdesk:mass_update"),
|
||||||
data={
|
data={"ticket_id": [str(ticket_1.id), str(ticket_2.id)], "action": "merge"},
|
||||||
'ticket_id': [str(ticket_1.id), str(ticket_2.id)],
|
follow=True,
|
||||||
'action': 'merge'
|
)
|
||||||
},
|
redirect_url = "%s?tickets=%s&tickets=%s" % (
|
||||||
follow=True
|
reverse("helpdesk:merge_tickets"),
|
||||||
|
ticket_1.id,
|
||||||
|
ticket_2.id,
|
||||||
)
|
)
|
||||||
redirect_url = '%s?tickets=%s&tickets=%s' % (
|
|
||||||
reverse('helpdesk:merge_tickets'), ticket_1.id, ticket_2.id)
|
|
||||||
self.assertRedirects(response, redirect_url)
|
self.assertRedirects(response, redirect_url)
|
||||||
self.assertContains(response, ticket_1.description)
|
self.assertContains(response, ticket_1.description)
|
||||||
self.assertContains(response, ticket_1.resolution)
|
self.assertContains(response, ticket_1.resolution)
|
||||||
@ -293,16 +302,16 @@ class TicketActionsTestCase(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
redirect_url,
|
redirect_url,
|
||||||
data={
|
data={
|
||||||
'chosen_ticket': str(ticket_1.id),
|
"chosen_ticket": str(ticket_1.id),
|
||||||
'due_date': str(ticket_2.id),
|
"due_date": str(ticket_2.id),
|
||||||
'status': str(ticket_1.id),
|
"status": str(ticket_1.id),
|
||||||
'submitter_email': str(ticket_2.id),
|
"submitter_email": str(ticket_2.id),
|
||||||
'description': str(ticket_2.id),
|
"description": str(ticket_2.id),
|
||||||
'assigned_to': str(ticket_2.id),
|
"assigned_to": str(ticket_2.id),
|
||||||
custom_field_1.name: str(ticket_1.id),
|
custom_field_1.name: str(ticket_1.id),
|
||||||
custom_field_2.name: str(ticket_2.id),
|
custom_field_2.name: str(ticket_2.id),
|
||||||
},
|
},
|
||||||
follow=True
|
follow=True,
|
||||||
)
|
)
|
||||||
self.assertRedirects(response, ticket_1.get_absolute_url())
|
self.assertRedirects(response, ticket_1.get_absolute_url())
|
||||||
ticket_2.refresh_from_db()
|
ticket_2.refresh_from_db()
|
||||||
@ -316,14 +325,18 @@ class TicketActionsTestCase(TestCase):
|
|||||||
self.assertEqual(ticket_1.submitter_email, ticket_2.submitter_email)
|
self.assertEqual(ticket_1.submitter_email, ticket_2.submitter_email)
|
||||||
self.assertEqual(ticket_1.description, ticket_2.description)
|
self.assertEqual(ticket_1.description, ticket_2.description)
|
||||||
self.assertEqual(ticket_1.assigned_to, ticket_2.assigned_to)
|
self.assertEqual(ticket_1.assigned_to, ticket_2.assigned_to)
|
||||||
self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(
|
self.assertEqual(
|
||||||
field=custom_field_1).value, ticket_1_field_1)
|
ticket_1.ticketcustomfieldvalue_set.get(field=custom_field_1).value,
|
||||||
self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(
|
ticket_1_field_1,
|
||||||
field=custom_field_2).value, ticket_2_field_2)
|
)
|
||||||
self.assertEqual(list(ticket_1.followup_set.all()), [
|
self.assertEqual(
|
||||||
ticket_1_follow_up, ticket_2_follow_up])
|
ticket_1.ticketcustomfieldvalue_set.get(field=custom_field_2).value,
|
||||||
self.assertEqual(list(ticket_1.ticketcc_set.all()),
|
ticket_2_field_2,
|
||||||
[ticket_1_cc, ticket_2_cc])
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
list(ticket_1.followup_set.all()), [ticket_1_follow_up, ticket_2_follow_up]
|
||||||
|
)
|
||||||
|
self.assertEqual(list(ticket_1.ticketcc_set.all()), [ticket_1_cc, ticket_2_cc])
|
||||||
|
|
||||||
def test_update_ticket_queue(self):
|
def test_update_ticket_queue(self):
|
||||||
"""Tests whether user can change the queue in the Respond to this ticket section."""
|
"""Tests whether user can change the queue in the Respond to this ticket section."""
|
||||||
@ -333,10 +346,10 @@ class TicketActionsTestCase(TestCase):
|
|||||||
|
|
||||||
# create ticket
|
# create ticket
|
||||||
initial_data = {
|
initial_data = {
|
||||||
'title': 'Queue change ticket test',
|
"title": "Queue change ticket test",
|
||||||
'queue': self.queue_public,
|
"queue": self.queue_public,
|
||||||
'assigned_to': self.user,
|
"assigned_to": self.user,
|
||||||
'status': Ticket.OPEN_STATUS,
|
"status": Ticket.OPEN_STATUS,
|
||||||
}
|
}
|
||||||
ticket = Ticket.objects.create(**initial_data)
|
ticket = Ticket.objects.create(**initial_data)
|
||||||
ticket_id = ticket.id
|
ticket_id = ticket.id
|
||||||
@ -346,24 +359,24 @@ class TicketActionsTestCase(TestCase):
|
|||||||
|
|
||||||
# POST first follow-up with new queue
|
# POST first follow-up with new queue
|
||||||
new_queue = Queue.objects.create(
|
new_queue = Queue.objects.create(
|
||||||
title='New Queue',
|
title="New Queue",
|
||||||
slug='newqueue',
|
slug="newqueue",
|
||||||
)
|
)
|
||||||
post_data = {
|
post_data = {
|
||||||
'comment': 'first follow-up in new queue',
|
"comment": "first follow-up in new queue",
|
||||||
'queue': str(new_queue.id),
|
"queue": str(new_queue.id),
|
||||||
}
|
}
|
||||||
response = self.client.post(reverse('helpdesk:update',
|
response = self.client.post(
|
||||||
kwargs={'ticket_id': ticket_id}),
|
reverse("helpdesk:update", kwargs={"ticket_id": ticket_id}), post_data
|
||||||
post_data)
|
)
|
||||||
|
|
||||||
# queue was correctly modified
|
# queue was correctly modified
|
||||||
ticket.refresh_from_db()
|
ticket.refresh_from_db()
|
||||||
self.assertEqual(ticket.queue, new_queue)
|
self.assertEqual(ticket.queue, new_queue)
|
||||||
|
|
||||||
# ticket change was saved
|
# ticket change was saved
|
||||||
latest_fup = ticket.followup_set.latest('date')
|
latest_fup = ticket.followup_set.latest("date")
|
||||||
latest_ticketchange = latest_fup.ticketchange_set.latest('id')
|
latest_ticketchange = latest_fup.ticketchange_set.latest("id")
|
||||||
self.assertEqual(latest_ticketchange.field, _('Queue'))
|
self.assertEqual(latest_ticketchange.field, _("Queue"))
|
||||||
self.assertEqual(int(latest_ticketchange.old_value), self.queue_public.id)
|
self.assertEqual(int(latest_ticketchange.old_value), self.queue_public.id)
|
||||||
self.assertEqual(int(latest_ticketchange.new_value), new_queue.id)
|
self.assertEqual(int(latest_ticketchange.new_value), new_queue.id)
|
||||||
|
@ -9,14 +9,12 @@ from helpdesk.models import Queue, Ticket
|
|||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(HELPDESK_VIEW_A_TICKET_PUBLIC=True)
|
||||||
HELPDESK_VIEW_A_TICKET_PUBLIC=True
|
|
||||||
)
|
|
||||||
class TestTicketLookupPublicEnabled(TestCase):
|
class TestTicketLookupPublicEnabled(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
q = Queue(title='Q1', slug='q1')
|
q = Queue(title="Q1", slug="q1")
|
||||||
q.save()
|
q.save()
|
||||||
t = Ticket(title='Test Ticket', submitter_email='test@domain.com')
|
t = Ticket(title="Test Ticket", submitter_email="test@domain.com")
|
||||||
t.queue = q
|
t.queue = q
|
||||||
t.save()
|
t.save()
|
||||||
self.ticket = t
|
self.ticket = t
|
||||||
@ -33,20 +31,26 @@ class TestTicketLookupPublicEnabled(TestCase):
|
|||||||
# we will exercise 'reverse' to lookup/build the URL
|
# we will exercise 'reverse' to lookup/build the URL
|
||||||
# from the ticket info we have
|
# from the ticket info we have
|
||||||
# http://example.com/helpdesk/view/?ticket=q1-1&email=None
|
# http://example.com/helpdesk/view/?ticket=q1-1&email=None
|
||||||
response = self.client.get(reverse('helpdesk:public_view'),
|
response = self.client.get(
|
||||||
{'ticket': self.ticket.ticket_for_url,
|
reverse("helpdesk:public_view"),
|
||||||
'email': self.ticket.submitter_email})
|
{
|
||||||
|
"ticket": self.ticket.ticket_for_url,
|
||||||
|
"email": self.ticket.submitter_email,
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_ticket_with_changed_queue(self):
|
def test_ticket_with_changed_queue(self):
|
||||||
# Make a ticket (already done in setup() )
|
# Make a ticket (already done in setup() )
|
||||||
# Now make another queue
|
# Now make another queue
|
||||||
q2 = Queue(title='Q2', slug='q2')
|
q2 = Queue(title="Q2", slug="q2")
|
||||||
q2.save()
|
q2.save()
|
||||||
# grab the URL / params which would have been emailed out to submitter.
|
# grab the URL / params which would have been emailed out to submitter.
|
||||||
url = reverse('helpdesk:public_view')
|
url = reverse("helpdesk:public_view")
|
||||||
params = {'ticket': self.ticket.ticket_for_url,
|
params = {
|
||||||
'email': self.ticket.submitter_email}
|
"ticket": self.ticket.ticket_for_url,
|
||||||
|
"email": self.ticket.submitter_email,
|
||||||
|
}
|
||||||
# Pickup the ticket created in setup() and change its queue
|
# Pickup the ticket created in setup() and change its queue
|
||||||
self.ticket.queue = q2
|
self.ticket.queue = q2
|
||||||
self.ticket.save()
|
self.ticket.save()
|
||||||
@ -56,36 +60,34 @@ class TestTicketLookupPublicEnabled(TestCase):
|
|||||||
self.assertNotContains(response, "Invalid ticket ID")
|
self.assertNotContains(response, "Invalid ticket ID")
|
||||||
|
|
||||||
def test_add_email_to_ticketcc_if_not_in(self):
|
def test_add_email_to_ticketcc_if_not_in(self):
|
||||||
staff_email = 'staff@mail.com'
|
staff_email = "staff@mail.com"
|
||||||
staff_user = User.objects.create(
|
staff_user = User.objects.create(
|
||||||
username='staff', email=staff_email, is_staff=True)
|
username="staff", email=staff_email, is_staff=True
|
||||||
|
)
|
||||||
self.ticket.assigned_to = staff_user
|
self.ticket.assigned_to = staff_user
|
||||||
self.ticket.save()
|
self.ticket.save()
|
||||||
email_1 = 'user1@mail.com'
|
email_1 = "user1@mail.com"
|
||||||
ticketcc_1 = self.ticket.ticketcc_set.create(email=email_1)
|
ticketcc_1 = self.ticket.ticketcc_set.create(email=email_1)
|
||||||
|
|
||||||
# Add new email to CC
|
# Add new email to CC
|
||||||
email_2 = 'user2@mail.com'
|
email_2 = "user2@mail.com"
|
||||||
ticketcc_2 = self.ticket.add_email_to_ticketcc_if_not_in(email=email_2)
|
ticketcc_2 = self.ticket.add_email_to_ticketcc_if_not_in(email=email_2)
|
||||||
self.assertEqual(list(self.ticket.ticketcc_set.all()),
|
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
|
||||||
[ticketcc_1, ticketcc_2])
|
|
||||||
|
|
||||||
# Add existing email, doesn't change anything
|
# Add existing email, doesn't change anything
|
||||||
self.ticket.add_email_to_ticketcc_if_not_in(email=email_1)
|
self.ticket.add_email_to_ticketcc_if_not_in(email=email_1)
|
||||||
self.assertEqual(list(self.ticket.ticketcc_set.all()),
|
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
|
||||||
[ticketcc_1, ticketcc_2])
|
|
||||||
|
|
||||||
# Add mail from assigned user, doesn't change anything
|
# Add mail from assigned user, doesn't change anything
|
||||||
self.ticket.add_email_to_ticketcc_if_not_in(email=staff_email)
|
self.ticket.add_email_to_ticketcc_if_not_in(email=staff_email)
|
||||||
self.assertEqual(list(self.ticket.ticketcc_set.all()),
|
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
|
||||||
[ticketcc_1, ticketcc_2])
|
|
||||||
self.ticket.add_email_to_ticketcc_if_not_in(user=staff_user)
|
self.ticket.add_email_to_ticketcc_if_not_in(user=staff_user)
|
||||||
self.assertEqual(list(self.ticket.ticketcc_set.all()),
|
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
|
||||||
[ticketcc_1, ticketcc_2])
|
|
||||||
|
|
||||||
# Move a ticketCC from ticket 1 to ticket 2
|
# Move a ticketCC from ticket 1 to ticket 2
|
||||||
ticket_2 = Ticket.objects.create(
|
ticket_2 = Ticket.objects.create(
|
||||||
queue=self.ticket.queue, title='Ticket 2', submitter_email=email_2)
|
queue=self.ticket.queue, title="Ticket 2", submitter_email=email_2
|
||||||
|
)
|
||||||
self.assertEqual(ticket_2.ticketcc_set.count(), 0)
|
self.assertEqual(ticket_2.ticketcc_set.count(), 0)
|
||||||
ticket_2.add_email_to_ticketcc_if_not_in(ticketcc=ticketcc_1)
|
ticket_2.add_email_to_ticketcc_if_not_in(ticketcc=ticketcc_1)
|
||||||
self.assertEqual(ticketcc_1.ticket, ticket_2)
|
self.assertEqual(ticketcc_1.ticket, ticket_2)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
@ -7,19 +6,19 @@ from django.test.client import Client
|
|||||||
from helpdesk.models import FollowUp, Queue, Ticket
|
from helpdesk.models import FollowUp, Queue, Ticket
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
class TimeSpentTestCase(TestCase):
|
|
||||||
|
|
||||||
|
class TimeSpentTestCase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.queue_public = Queue.objects.create(
|
self.queue_public = Queue.objects.create(
|
||||||
title='Queue 1',
|
title="Queue 1",
|
||||||
slug='q1',
|
slug="q1",
|
||||||
allow_public_submission=True,
|
allow_public_submission=True,
|
||||||
dedicated_time=datetime.timedelta(minutes=60)
|
dedicated_time=datetime.timedelta(minutes=60),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.ticket_data = {
|
self.ticket_data = {
|
||||||
'title': 'Test Ticket',
|
"title": "Test Ticket",
|
||||||
'description': 'Some Test Ticket',
|
"description": "Some Test Ticket",
|
||||||
}
|
}
|
||||||
|
|
||||||
ticket_data = dict(queue=self.queue_public, **self.ticket_data)
|
ticket_data = dict(queue=self.queue_public, **self.ticket_data)
|
||||||
@ -28,12 +27,12 @@ class TimeSpentTestCase(TestCase):
|
|||||||
self.client = Client()
|
self.client = Client()
|
||||||
|
|
||||||
user1_kwargs = {
|
user1_kwargs = {
|
||||||
'username': 'staff',
|
"username": "staff",
|
||||||
'email': 'staff@example.com',
|
"email": "staff@example.com",
|
||||||
'password': make_password('Test1234'),
|
"password": make_password("Test1234"),
|
||||||
'is_staff': True,
|
"is_staff": True,
|
||||||
'is_superuser': False,
|
"is_superuser": False,
|
||||||
'is_active': True
|
"is_active": True,
|
||||||
}
|
}
|
||||||
self.user = User.objects.create(**user1_kwargs)
|
self.user = User.objects.create(**user1_kwargs)
|
||||||
|
|
||||||
@ -50,7 +49,7 @@ class TimeSpentTestCase(TestCase):
|
|||||||
user=self.user,
|
user=self.user,
|
||||||
new_status=1,
|
new_status=1,
|
||||||
message_id=message_id,
|
message_id=message_id,
|
||||||
time_spent=datetime.timedelta(minutes=30)
|
time_spent=datetime.timedelta(minutes=30),
|
||||||
)
|
)
|
||||||
|
|
||||||
followup.save()
|
followup.save()
|
||||||
@ -59,5 +58,6 @@ class TimeSpentTestCase(TestCase):
|
|||||||
self.assertEqual(self.ticket.time_spent.seconds, 1800)
|
self.assertEqual(self.ticket.time_spent.seconds, 1800)
|
||||||
self.assertEqual(self.queue_public.time_spent.seconds, 1800)
|
self.assertEqual(self.queue_public.time_spent.seconds, 1800)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
self.queue_public.dedicated_time.seconds > self.queue_public.time_spent.seconds
|
self.queue_public.dedicated_time.seconds
|
||||||
|
> self.queue_public.time_spent.seconds
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
@ -13,43 +12,42 @@ import uuid
|
|||||||
|
|
||||||
@override_settings(USE_TZ=True)
|
@override_settings(USE_TZ=True)
|
||||||
class TimeSpentAutoTestCase(TestCase):
|
class TimeSpentAutoTestCase(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Creates a queue, ticket and user."""
|
"""Creates a queue, ticket and user."""
|
||||||
self.queue_public = Queue.objects.create(
|
self.queue_public = Queue.objects.create(
|
||||||
title='Queue 1',
|
title="Queue 1",
|
||||||
slug='q1',
|
slug="q1",
|
||||||
allow_public_submission=True,
|
allow_public_submission=True,
|
||||||
dedicated_time=timedelta(minutes=60)
|
dedicated_time=timedelta(minutes=60),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.ticket_data = dict(queue=self.queue_public,
|
self.ticket_data = dict(
|
||||||
title='test ticket',
|
queue=self.queue_public,
|
||||||
description='test ticket description')
|
title="test ticket",
|
||||||
|
description="test ticket description",
|
||||||
|
)
|
||||||
|
|
||||||
self.user = User.objects.create(
|
self.user = User.objects.create(
|
||||||
username='staff',
|
username="staff",
|
||||||
email='staff@example.com',
|
email="staff@example.com",
|
||||||
password=make_password('Test1234'),
|
password=make_password("Test1234"),
|
||||||
is_staff=True,
|
is_staff=True,
|
||||||
is_superuser=False,
|
is_superuser=False,
|
||||||
is_active=True
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.client = Client()
|
self.client = Client()
|
||||||
|
|
||||||
|
|
||||||
def loginUser(self, is_staff=True):
|
def loginUser(self, is_staff=True):
|
||||||
"""Create a staff user and login"""
|
"""Create a staff user and login"""
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
self.user = User.objects.create(
|
self.user = User.objects.create(
|
||||||
username='User_1',
|
username="User_1",
|
||||||
is_staff=is_staff,
|
is_staff=is_staff,
|
||||||
)
|
)
|
||||||
self.user.set_password('pass')
|
self.user.set_password("pass")
|
||||||
self.user.save()
|
self.user.save()
|
||||||
self.client.login(username='User_1', password='pass')
|
self.client.login(username="User_1", password="pass")
|
||||||
|
|
||||||
|
|
||||||
def test_add_two_followups_time_spent_auto(self):
|
def test_add_two_followups_time_spent_auto(self):
|
||||||
"""Tests automatic time_spent calculation."""
|
"""Tests automatic time_spent calculation."""
|
||||||
@ -59,15 +57,39 @@ class TimeSpentAutoTestCase(TestCase):
|
|||||||
# ticket creation date, follow-up creation date, assertion value
|
# ticket creation date, follow-up creation date, assertion value
|
||||||
TEST_VALUES = (
|
TEST_VALUES = (
|
||||||
# friday
|
# friday
|
||||||
('2024-03-01T00:00:00+00:00', '2024-03-01T09:30:10+00:00', timedelta(hours=9, minutes=30, seconds=10)),
|
(
|
||||||
('2024-03-01T00:00:00+00:00', '2024-03-01T23:59:58+00:00', timedelta(hours=23, minutes=59, seconds=58)),
|
"2024-03-01T00:00:00+00:00",
|
||||||
('2024-03-01T00:00:00+00:00', '2024-03-01T23:59:59+00:00', timedelta(hours=23, minutes=59, seconds=59)),
|
"2024-03-01T09:30:10+00:00",
|
||||||
('2024-03-01T00:00:00+00:00', '2024-03-02T00:00:00+00:00', timedelta(hours=24)),
|
timedelta(hours=9, minutes=30, seconds=10),
|
||||||
('2024-03-01T00:00:00+00:00', '2024-03-02T09:00:00+00:00', timedelta(hours=33)),
|
),
|
||||||
('2024-03-01T00:00:00+00:00', '2024-03-03T00:00:00+00:00', timedelta(hours=48)),
|
(
|
||||||
|
"2024-03-01T00:00:00+00:00",
|
||||||
|
"2024-03-01T23:59:58+00:00",
|
||||||
|
timedelta(hours=23, minutes=59, seconds=58),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2024-03-01T00:00:00+00:00",
|
||||||
|
"2024-03-01T23:59:59+00:00",
|
||||||
|
timedelta(hours=23, minutes=59, seconds=59),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2024-03-01T00:00:00+00:00",
|
||||||
|
"2024-03-02T00:00:00+00:00",
|
||||||
|
timedelta(hours=24),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2024-03-01T00:00:00+00:00",
|
||||||
|
"2024-03-02T09:00:00+00:00",
|
||||||
|
timedelta(hours=33),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2024-03-01T00:00:00+00:00",
|
||||||
|
"2024-03-03T00:00:00+00:00",
|
||||||
|
timedelta(hours=48),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
for (ticket_time, fup_time, assertion_delta) in TEST_VALUES:
|
for ticket_time, fup_time, assertion_delta in TEST_VALUES:
|
||||||
# create and setup test ticket time
|
# create and setup test ticket time
|
||||||
ticket = Ticket.objects.create(**self.ticket_data)
|
ticket = Ticket.objects.create(**self.ticket_data)
|
||||||
ticket_time_p = datetime.strptime(ticket_time, "%Y-%m-%dT%H:%M:%S%z")
|
ticket_time_p = datetime.strptime(ticket_time, "%Y-%m-%dT%H:%M:%S%z")
|
||||||
@ -85,15 +107,24 @@ class TimeSpentAutoTestCase(TestCase):
|
|||||||
user=self.user,
|
user=self.user,
|
||||||
new_status=1,
|
new_status=1,
|
||||||
message_id=uuid.uuid4().hex,
|
message_id=uuid.uuid4().hex,
|
||||||
time_spent=None
|
time_spent=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(followup1.time_spent.total_seconds(), assertion_delta.total_seconds())
|
self.assertEqual(
|
||||||
self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds())
|
followup1.time_spent.total_seconds(), assertion_delta.total_seconds()
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
ticket.time_spent.total_seconds(), assertion_delta.total_seconds()
|
||||||
|
)
|
||||||
|
|
||||||
# adding a second follow-up at different intervals
|
# adding a second follow-up at different intervals
|
||||||
for delta in (timedelta(seconds=1), timedelta(minutes=1), timedelta(hours=1), timedelta(days=1), timedelta(days=10)):
|
for delta in (
|
||||||
|
timedelta(seconds=1),
|
||||||
|
timedelta(minutes=1),
|
||||||
|
timedelta(hours=1),
|
||||||
|
timedelta(days=1),
|
||||||
|
timedelta(days=10),
|
||||||
|
):
|
||||||
followup2 = FollowUp.objects.create(
|
followup2 = FollowUp.objects.create(
|
||||||
ticket=ticket,
|
ticket=ticket,
|
||||||
date=followup1.date + delta,
|
date=followup1.date + delta,
|
||||||
@ -103,16 +134,20 @@ class TimeSpentAutoTestCase(TestCase):
|
|||||||
user=self.user,
|
user=self.user,
|
||||||
new_status=1,
|
new_status=1,
|
||||||
message_id=uuid.uuid4().hex,
|
message_id=uuid.uuid4().hex,
|
||||||
time_spent=None
|
time_spent=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(followup2.time_spent.total_seconds(), delta.total_seconds())
|
self.assertEqual(
|
||||||
self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds() + delta.total_seconds())
|
followup2.time_spent.total_seconds(), delta.total_seconds()
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
ticket.time_spent.total_seconds(),
|
||||||
|
assertion_delta.total_seconds() + delta.total_seconds(),
|
||||||
|
)
|
||||||
|
|
||||||
# delete second follow-up as we test it with many intervals
|
# delete second follow-up as we test it with many intervals
|
||||||
followup2.delete()
|
followup2.delete()
|
||||||
|
|
||||||
|
|
||||||
def test_followup_time_spent_auto_opening_hours(self):
|
def test_followup_time_spent_auto_opening_hours(self):
|
||||||
"""Tests automatic time_spent calculation with opening hours and holidays."""
|
"""Tests automatic time_spent calculation with opening hours and holidays."""
|
||||||
|
|
||||||
@ -130,45 +165,118 @@ class TimeSpentAutoTestCase(TestCase):
|
|||||||
|
|
||||||
# adding holidays
|
# adding holidays
|
||||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = (
|
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = (
|
||||||
'2024-03-18', '2024-03-19', '2024-03-20', '2024-03-21', '2024-03-22',
|
"2024-03-18",
|
||||||
|
"2024-03-19",
|
||||||
|
"2024-03-20",
|
||||||
|
"2024-03-21",
|
||||||
|
"2024-03-22",
|
||||||
)
|
)
|
||||||
|
|
||||||
# ticket creation date, follow-up creation date, assertion value
|
# ticket creation date, follow-up creation date, assertion value
|
||||||
TEST_VALUES = (
|
TEST_VALUES = (
|
||||||
# monday
|
# monday
|
||||||
('2024-03-04T00:00:00+00:00', '2024-03-04T09:30:10+00:00', timedelta(hours=9, minutes=30, seconds=10)),
|
(
|
||||||
|
"2024-03-04T00:00:00+00:00",
|
||||||
|
"2024-03-04T09:30:10+00:00",
|
||||||
|
timedelta(hours=9, minutes=30, seconds=10),
|
||||||
|
),
|
||||||
# tuesday
|
# tuesday
|
||||||
('2024-03-05T07:00:00+00:00', '2024-03-05T09:00:00+00:00', timedelta(hours=1)),
|
(
|
||||||
('2024-03-05T17:50:00+00:00', '2024-03-05T17:51:00+00:00', timedelta(minutes=1)),
|
"2024-03-05T07:00:00+00:00",
|
||||||
('2024-03-05T17:50:00+00:00', '2024-03-05T19:51:00+00:00', timedelta(minutes=10)),
|
"2024-03-05T09:00:00+00:00",
|
||||||
('2024-03-05T18:00:00+00:00', '2024-03-05T23:59:59+00:00', timedelta(hours=0)),
|
timedelta(hours=1),
|
||||||
('2024-03-05T20:00:00+00:00', '2024-03-05T20:59:59+00:00', timedelta(hours=0)),
|
),
|
||||||
|
(
|
||||||
|
"2024-03-05T17:50:00+00:00",
|
||||||
|
"2024-03-05T17:51:00+00:00",
|
||||||
|
timedelta(minutes=1),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2024-03-05T17:50:00+00:00",
|
||||||
|
"2024-03-05T19:51:00+00:00",
|
||||||
|
timedelta(minutes=10),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2024-03-05T18:00:00+00:00",
|
||||||
|
"2024-03-05T23:59:59+00:00",
|
||||||
|
timedelta(hours=0),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2024-03-05T20:00:00+00:00",
|
||||||
|
"2024-03-05T20:59:59+00:00",
|
||||||
|
timedelta(hours=0),
|
||||||
|
),
|
||||||
# wednesday
|
# wednesday
|
||||||
('2024-03-06T08:00:00+00:00', '2024-03-06T09:01:00+00:00', timedelta(minutes=31)),
|
(
|
||||||
('2024-03-06T01:00:00+00:00', '2024-03-06T19:30:10+00:00', timedelta(hours=10)),
|
"2024-03-06T08:00:00+00:00",
|
||||||
('2024-03-06T18:01:00+00:00', '2024-03-06T19:00:00+00:00', timedelta(minutes=29)),
|
"2024-03-06T09:01:00+00:00",
|
||||||
|
timedelta(minutes=31),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2024-03-06T01:00:00+00:00",
|
||||||
|
"2024-03-06T19:30:10+00:00",
|
||||||
|
timedelta(hours=10),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2024-03-06T18:01:00+00:00",
|
||||||
|
"2024-03-06T19:00:00+00:00",
|
||||||
|
timedelta(minutes=29),
|
||||||
|
),
|
||||||
# thursday
|
# thursday
|
||||||
('2024-03-07T00:00:00+00:00', '2024-03-07T09:30:10+00:00', timedelta(hours=9, minutes=30, seconds=10)),
|
(
|
||||||
('2024-03-07T09:30:00+00:00', '2024-03-07T10:30:00+00:00', timedelta(minutes=30)),
|
"2024-03-07T00:00:00+00:00",
|
||||||
|
"2024-03-07T09:30:10+00:00",
|
||||||
|
timedelta(hours=9, minutes=30, seconds=10),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2024-03-07T09:30:00+00:00",
|
||||||
|
"2024-03-07T10:30:00+00:00",
|
||||||
|
timedelta(minutes=30),
|
||||||
|
),
|
||||||
# friday
|
# friday
|
||||||
('2024-03-08T00:00:00+00:00', '2024-03-08T23:30:10+00:00', timedelta(hours=10)),
|
(
|
||||||
|
"2024-03-08T00:00:00+00:00",
|
||||||
|
"2024-03-08T23:30:10+00:00",
|
||||||
|
timedelta(hours=10),
|
||||||
|
),
|
||||||
# saturday
|
# saturday
|
||||||
('2024-03-09T00:00:00+00:00', '2024-03-09T09:30:10+00:00', timedelta(hours=0)),
|
(
|
||||||
|
"2024-03-09T00:00:00+00:00",
|
||||||
|
"2024-03-09T09:30:10+00:00",
|
||||||
|
timedelta(hours=0),
|
||||||
|
),
|
||||||
# sunday
|
# sunday
|
||||||
('2024-03-10T00:00:00+00:00', '2024-03-10T09:30:10+00:00', timedelta(hours=0)),
|
(
|
||||||
|
"2024-03-10T00:00:00+00:00",
|
||||||
|
"2024-03-10T09:30:10+00:00",
|
||||||
|
timedelta(hours=0),
|
||||||
|
),
|
||||||
# monday to sunday
|
# monday to sunday
|
||||||
('2024-03-04T04:00:00+00:00', '2024-03-10T09:00:00+00:00', timedelta(hours=60)),
|
(
|
||||||
|
"2024-03-04T04:00:00+00:00",
|
||||||
|
"2024-03-10T09:00:00+00:00",
|
||||||
|
timedelta(hours=60),
|
||||||
|
),
|
||||||
# two weeks
|
# two weeks
|
||||||
('2024-03-04T04:00:00+00:00', '2024-03-17T09:00:00+00:00', timedelta(hours=124)),
|
(
|
||||||
|
"2024-03-04T04:00:00+00:00",
|
||||||
|
"2024-03-17T09:00:00+00:00",
|
||||||
|
timedelta(hours=124),
|
||||||
|
),
|
||||||
# three weeks, the third one is holidays
|
# three weeks, the third one is holidays
|
||||||
('2024-03-04T04:00:00+00:00', '2024-03-24T09:00:00+00:00', timedelta(hours=124)),
|
(
|
||||||
('2024-03-18T04:00:00+00:00', '2024-03-24T09:00:00+00:00', timedelta(hours=0)),
|
"2024-03-04T04:00:00+00:00",
|
||||||
|
"2024-03-24T09:00:00+00:00",
|
||||||
|
timedelta(hours=124),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2024-03-18T04:00:00+00:00",
|
||||||
|
"2024-03-24T09:00:00+00:00",
|
||||||
|
timedelta(hours=0),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
for (ticket_time, fup_time, assertion_delta) in TEST_VALUES:
|
for ticket_time, fup_time, assertion_delta in TEST_VALUES:
|
||||||
# create and setup test ticket time
|
# create and setup test ticket time
|
||||||
ticket = Ticket.objects.create(**self.ticket_data)
|
ticket = Ticket.objects.create(**self.ticket_data)
|
||||||
ticket_time_p = datetime.strptime(ticket_time, "%Y-%m-%dT%H:%M:%S%z")
|
ticket_time_p = datetime.strptime(ticket_time, "%Y-%m-%dT%H:%M:%S%z")
|
||||||
@ -186,11 +294,15 @@ class TimeSpentAutoTestCase(TestCase):
|
|||||||
user=self.user,
|
user=self.user,
|
||||||
new_status=1,
|
new_status=1,
|
||||||
message_id=uuid.uuid4().hex,
|
message_id=uuid.uuid4().hex,
|
||||||
time_spent=None
|
time_spent=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(followup1.time_spent.total_seconds(), assertion_delta.total_seconds())
|
self.assertEqual(
|
||||||
self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds())
|
followup1.time_spent.total_seconds(), assertion_delta.total_seconds()
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
ticket.time_spent.total_seconds(), assertion_delta.total_seconds()
|
||||||
|
)
|
||||||
|
|
||||||
# removing opening hours and holidays
|
# removing opening hours and holidays
|
||||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS = {}
|
helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS = {}
|
||||||
@ -205,15 +317,18 @@ class TimeSpentAutoTestCase(TestCase):
|
|||||||
# Follow-ups with OPEN_STATUS are excluded from time counting
|
# Follow-ups with OPEN_STATUS are excluded from time counting
|
||||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = (Ticket.OPEN_STATUS,)
|
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = (Ticket.OPEN_STATUS,)
|
||||||
|
|
||||||
|
|
||||||
# create and setup test ticket time
|
# create and setup test ticket time
|
||||||
ticket = Ticket.objects.create(**self.ticket_data)
|
ticket = Ticket.objects.create(**self.ticket_data)
|
||||||
ticket_time_p = datetime.strptime('2024-03-04T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z")
|
ticket_time_p = datetime.strptime(
|
||||||
|
"2024-03-04T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"
|
||||||
|
)
|
||||||
ticket.created = ticket_time_p
|
ticket.created = ticket_time_p
|
||||||
ticket.modified = ticket_time_p
|
ticket.modified = ticket_time_p
|
||||||
ticket.save()
|
ticket.save()
|
||||||
|
|
||||||
fup_time_p = datetime.strptime('2024-03-10T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z")
|
fup_time_p = datetime.strptime(
|
||||||
|
"2024-03-10T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"
|
||||||
|
)
|
||||||
followup1 = FollowUp.objects.create(
|
followup1 = FollowUp.objects.create(
|
||||||
ticket=ticket,
|
ticket=ticket,
|
||||||
date=fup_time_p,
|
date=fup_time_p,
|
||||||
@ -223,7 +338,7 @@ class TimeSpentAutoTestCase(TestCase):
|
|||||||
user=self.user,
|
user=self.user,
|
||||||
new_status=1,
|
new_status=1,
|
||||||
message_id=uuid.uuid4().hex,
|
message_id=uuid.uuid4().hex,
|
||||||
time_spent=None
|
time_spent=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# The Follow-up time_spent should be zero as the default OPEN_STATUS was excluded from calculation
|
# The Follow-up time_spent should be zero as the default OPEN_STATUS was excluded from calculation
|
||||||
@ -233,7 +348,6 @@ class TimeSpentAutoTestCase(TestCase):
|
|||||||
# Remove status exclusion
|
# Remove status exclusion
|
||||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = ()
|
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = ()
|
||||||
|
|
||||||
|
|
||||||
def test_followup_time_spent_auto_exclude_queues(self):
|
def test_followup_time_spent_auto_exclude_queues(self):
|
||||||
"""Tests automatic time_spent calculation queues exclusion."""
|
"""Tests automatic time_spent calculation queues exclusion."""
|
||||||
|
|
||||||
@ -241,17 +355,20 @@ class TimeSpentAutoTestCase(TestCase):
|
|||||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True
|
helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True
|
||||||
|
|
||||||
# Follow-ups within the default queue are excluded from time counting
|
# Follow-ups within the default queue are excluded from time counting
|
||||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ('q1',)
|
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ("q1",)
|
||||||
|
|
||||||
|
|
||||||
# create and setup test ticket time
|
# create and setup test ticket time
|
||||||
ticket = Ticket.objects.create(**self.ticket_data)
|
ticket = Ticket.objects.create(**self.ticket_data)
|
||||||
ticket_time_p = datetime.strptime('2024-03-04T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z")
|
ticket_time_p = datetime.strptime(
|
||||||
|
"2024-03-04T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"
|
||||||
|
)
|
||||||
ticket.created = ticket_time_p
|
ticket.created = ticket_time_p
|
||||||
ticket.modified = ticket_time_p
|
ticket.modified = ticket_time_p
|
||||||
ticket.save()
|
ticket.save()
|
||||||
|
|
||||||
fup_time_p = datetime.strptime('2024-03-10T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z")
|
fup_time_p = datetime.strptime(
|
||||||
|
"2024-03-10T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"
|
||||||
|
)
|
||||||
followup1 = FollowUp.objects.create(
|
followup1 = FollowUp.objects.create(
|
||||||
ticket=ticket,
|
ticket=ticket,
|
||||||
date=fup_time_p,
|
date=fup_time_p,
|
||||||
@ -261,7 +378,7 @@ class TimeSpentAutoTestCase(TestCase):
|
|||||||
user=self.user,
|
user=self.user,
|
||||||
new_status=1,
|
new_status=1,
|
||||||
message_id=uuid.uuid4().hex,
|
message_id=uuid.uuid4().hex,
|
||||||
time_spent=None
|
time_spent=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# The Follow-up time_spent should be zero as the default queue was excluded from calculation
|
# The Follow-up time_spent should be zero as the default queue was excluded from calculation
|
||||||
@ -276,13 +393,13 @@ class TimeSpentAutoTestCase(TestCase):
|
|||||||
|
|
||||||
# activate automatic calculation
|
# activate automatic calculation
|
||||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True
|
helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True
|
||||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ('stop1', 'stop2')
|
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ("stop1", "stop2")
|
||||||
|
|
||||||
# make staff user
|
# make staff user
|
||||||
self.loginUser()
|
self.loginUser()
|
||||||
|
|
||||||
# create queues
|
# create queues
|
||||||
queues_sequence = ('new', 'stop1', 'resume1', 'stop2', 'resume2', 'end')
|
queues_sequence = ("new", "stop1", "resume1", "stop2", "resume2", "end")
|
||||||
queues = dict()
|
queues = dict()
|
||||||
for slug in queues_sequence:
|
for slug in queues_sequence:
|
||||||
queues[slug] = Queue.objects.create(
|
queues[slug] = Queue.objects.create(
|
||||||
@ -292,34 +409,39 @@ class TimeSpentAutoTestCase(TestCase):
|
|||||||
|
|
||||||
# create ticket
|
# create ticket
|
||||||
initial_data = {
|
initial_data = {
|
||||||
'title': 'Queue change ticket test',
|
"title": "Queue change ticket test",
|
||||||
'queue': queues['new'],
|
"queue": queues["new"],
|
||||||
'assigned_to': self.user,
|
"assigned_to": self.user,
|
||||||
'status': Ticket.OPEN_STATUS,
|
"status": Ticket.OPEN_STATUS,
|
||||||
'created': datetime.strptime('2024-04-09T08:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z")
|
"created": datetime.strptime(
|
||||||
|
"2024-04-09T08:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
ticket = Ticket.objects.create(**initial_data)
|
ticket = Ticket.objects.create(**initial_data)
|
||||||
|
|
||||||
# create a change queue follow-up every hour
|
# create a change queue follow-up every hour
|
||||||
# first follow-up created at the same time of the ticket without queue change
|
# first follow-up created at the same time of the ticket without queue change
|
||||||
# new --1h--> stop1 --0h--> resume1 --1h--> stop2 --0h--> resume2 --1h--> end
|
# new --1h--> stop1 --0h--> resume1 --1h--> stop2 --0h--> resume2 --1h--> end
|
||||||
for (i, queue) in enumerate(queues_sequence):
|
for i, queue in enumerate(queues_sequence):
|
||||||
# create follow-up
|
# create follow-up
|
||||||
post_data = {
|
post_data = {
|
||||||
'comment': 'ticket in queue {}'.format(queue),
|
"comment": "ticket in queue {}".format(queue),
|
||||||
'queue': queues[queue].id,
|
"queue": queues[queue].id,
|
||||||
}
|
}
|
||||||
response = self.client.post(reverse('helpdesk:update', kwargs={
|
response = self.client.post(
|
||||||
'ticket_id': ticket.id}), post_data)
|
reverse("helpdesk:update", kwargs={"ticket_id": ticket.id}), post_data
|
||||||
latest_fup = ticket.followup_set.latest('id')
|
)
|
||||||
|
latest_fup = ticket.followup_set.latest("id")
|
||||||
latest_fup.date = ticket.created + timedelta(hours=i)
|
latest_fup.date = ticket.created + timedelta(hours=i)
|
||||||
latest_fup.time_spent = None
|
latest_fup.time_spent = None
|
||||||
latest_fup.save()
|
latest_fup.save()
|
||||||
|
|
||||||
# total ticket time for followups is 5 hours
|
# total ticket time for followups is 5 hours
|
||||||
self.assertEqual(latest_fup.date - ticket.created, timedelta(hours=5))
|
self.assertEqual(latest_fup.date - ticket.created, timedelta(hours=5))
|
||||||
# calculated time spent with 2 hours exclusion is 3 hours
|
# calculated time spent with 2 hours exclusion is 3 hours
|
||||||
self.assertEqual(ticket.time_spent.total_seconds(), timedelta(hours=3).total_seconds())
|
self.assertEqual(
|
||||||
|
ticket.time_spent.total_seconds(), timedelta(hours=3).total_seconds()
|
||||||
|
)
|
||||||
|
|
||||||
# remove queues exclusion
|
# remove queues exclusion
|
||||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ()
|
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ()
|
||||||
|
@ -4,20 +4,18 @@ from django.urls import reverse
|
|||||||
|
|
||||||
|
|
||||||
class TicketActionsTestCase(TestCase):
|
class TicketActionsTestCase(TestCase):
|
||||||
fixtures = ['emailtemplate.json']
|
fixtures = ["emailtemplate.json"]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
self.user = User.objects.create(
|
self.user = User.objects.create(
|
||||||
username='User_1',
|
username="User_1",
|
||||||
is_staff=True,
|
is_staff=True,
|
||||||
)
|
)
|
||||||
self.user.set_password('pass')
|
self.user.set_password("pass")
|
||||||
self.user.save()
|
self.user.save()
|
||||||
self.client.login(username='User_1', password='pass')
|
self.client.login(username="User_1", password="pass")
|
||||||
|
|
||||||
def test_get_user_settings(self):
|
def test_get_user_settings(self):
|
||||||
|
response = self.client.get(reverse("helpdesk:user_settings"), follow=True)
|
||||||
response = self.client.get(
|
|
||||||
reverse('helpdesk:user_settings'), follow=True)
|
|
||||||
self.assertContains(response, "Use the following options")
|
self.assertContains(response, "Use the following options")
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from helpdesk.models import Queue, CustomField, TicketCustomFieldValue, Ticket
|
from helpdesk.models import Queue, CustomField, TicketCustomFieldValue, Ticket
|
||||||
from helpdesk.serializers import TicketSerializer
|
from helpdesk.serializers import TicketSerializer
|
||||||
from rest_framework.status import (
|
from rest_framework.status import HTTP_201_CREATED
|
||||||
HTTP_201_CREATED
|
|
||||||
)
|
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@ -15,42 +13,53 @@ import http.server
|
|||||||
import threading
|
import threading
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
|
||||||
class WebhookRequestHandler(http.server.BaseHTTPRequestHandler):
|
class WebhookRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||||
server: "WebhookServer"
|
server: "WebhookServer"
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
content_length = int(self.headers['Content-Length'])
|
content_length = int(self.headers["Content-Length"])
|
||||||
body = self.rfile.read(content_length)
|
body = self.rfile.read(content_length)
|
||||||
self.server.requests.append({
|
self.server.requests.append(
|
||||||
'path': self.path,
|
{"path": self.path, "headers": self.headers, "body": body}
|
||||||
'headers': self.headers,
|
)
|
||||||
'body': body
|
if self.path == "/new-ticket":
|
||||||
})
|
self.server.handled_new_ticket_requests.append(
|
||||||
if self.path == '/new-ticket':
|
json.loads(body.decode("utf-8"))
|
||||||
self.server.handled_new_ticket_requests.append(json.loads(body.decode('utf-8')))
|
)
|
||||||
if self.path == '/new-ticket-1':
|
if self.path == "/new-ticket-1":
|
||||||
self.server.handled_new_ticket_requests_1.append(json.loads(body.decode('utf-8')))
|
self.server.handled_new_ticket_requests_1.append(
|
||||||
elif self.path == '/followup':
|
json.loads(body.decode("utf-8"))
|
||||||
self.server.handled_follow_up_requests.append(json.loads(body.decode('utf-8')))
|
)
|
||||||
elif self.path == '/followup-1':
|
elif self.path == "/followup":
|
||||||
self.server.handled_follow_up_requests_1.append(json.loads(body.decode('utf-8')))
|
self.server.handled_follow_up_requests.append(
|
||||||
|
json.loads(body.decode("utf-8"))
|
||||||
|
)
|
||||||
|
elif self.path == "/followup-1":
|
||||||
|
self.server.handled_follow_up_requests_1.append(
|
||||||
|
json.loads(body.decode("utf-8"))
|
||||||
|
)
|
||||||
self.send_response(HTTPStatus.OK)
|
self.send_response(HTTPStatus.OK)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if not self.path == '/get-past-requests':
|
if not self.path == "/get-past-requests":
|
||||||
self.send_response(HTTPStatus.NOT_FOUND)
|
self.send_response(HTTPStatus.NOT_FOUND)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
return
|
return
|
||||||
self.send_response(HTTPStatus.OK)
|
self.send_response(HTTPStatus.OK)
|
||||||
self.send_header('Content-type', 'application/json')
|
self.send_header("Content-type", "application/json")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(json.dumps({
|
self.wfile.write(
|
||||||
'new_ticket_requests': self.server.handled_new_ticket_requests,
|
json.dumps(
|
||||||
'new_ticket_requests_1': self.server.handled_new_ticket_requests_1,
|
{
|
||||||
'follow_up_requests': self.server.handled_follow_up_requests,
|
"new_ticket_requests": self.server.handled_new_ticket_requests,
|
||||||
'follow_up_requests_1': self.server.handled_follow_up_requests_1
|
"new_ticket_requests_1": self.server.handled_new_ticket_requests_1,
|
||||||
}).encode('utf-8'))
|
"follow_up_requests": self.server.handled_follow_up_requests,
|
||||||
|
"follow_up_requests_1": self.server.handled_follow_up_requests_1,
|
||||||
|
}
|
||||||
|
).encode("utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WebhookServer(http.server.HTTPServer):
|
class WebhookServer(http.server.HTTPServer):
|
||||||
@ -64,7 +73,9 @@ class WebhookServer(http.server.HTTPServer):
|
|||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.thread = threading.Thread(target=self.serve_forever)
|
self.thread = threading.Thread(target=self.serve_forever)
|
||||||
self.thread.daemon = True # Set as a daemon so it will be killed once the main thread is dead
|
self.thread.daemon = (
|
||||||
|
True # Set as a daemon so it will be killed once the main thread is dead
|
||||||
|
)
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@ -77,12 +88,12 @@ class WebhookTest(APITestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.queue = Queue.objects.create(
|
cls.queue = Queue.objects.create(
|
||||||
title='Test Queue',
|
title="Test Queue",
|
||||||
slug='test-queue',
|
slug="test-queue",
|
||||||
)
|
)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
staff_user = User.objects.create_user(username="test", is_staff=True)
|
||||||
CustomField(
|
CustomField(
|
||||||
name="my_custom_field",
|
name="my_custom_field",
|
||||||
data_type="varchar",
|
data_type="varchar",
|
||||||
@ -91,82 +102,135 @@ class WebhookTest(APITestCase):
|
|||||||
self.client.force_authenticate(staff_user)
|
self.client.force_authenticate(staff_user)
|
||||||
|
|
||||||
def test_test_server(self):
|
def test_test_server(self):
|
||||||
server = WebhookServer(('localhost', 8123), WebhookRequestHandler)
|
server = WebhookServer(("localhost", 8123), WebhookRequestHandler)
|
||||||
server.start()
|
server.start()
|
||||||
requests.post('http://localhost:8123/new-ticket', json={
|
requests.post("http://localhost:8123/new-ticket", json={"foo": "bar"})
|
||||||
"foo": "bar"})
|
handled_webhook_requests = requests.get(
|
||||||
handled_webhook_requests = requests.get('http://localhost:8123/get-past-requests').json()
|
"http://localhost:8123/get-past-requests"
|
||||||
self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["foo"], "bar")
|
).json()
|
||||||
|
self.assertEqual(
|
||||||
|
handled_webhook_requests["new_ticket_requests"][-1]["foo"], "bar"
|
||||||
|
)
|
||||||
server.stop()
|
server.stop()
|
||||||
|
|
||||||
def test_create_ticket_and_followup_via_api(self):
|
def test_create_ticket_and_followup_via_api(self):
|
||||||
server = WebhookServer(('localhost', 8124), WebhookRequestHandler)
|
server = WebhookServer(("localhost", 8124), WebhookRequestHandler)
|
||||||
os.environ['HELPDESK_NEW_TICKET_WEBHOOK_URLS'] = 'http://localhost:8124/new-ticket, http://localhost:8124/new-ticket-1'
|
os.environ["HELPDESK_NEW_TICKET_WEBHOOK_URLS"] = (
|
||||||
os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8124/followup , http://localhost:8124/followup-1'
|
"http://localhost:8124/new-ticket, http://localhost:8124/new-ticket-1"
|
||||||
|
)
|
||||||
|
os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = (
|
||||||
|
"http://localhost:8124/followup , http://localhost:8124/followup-1"
|
||||||
|
)
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
response = self.client.post('/api/tickets/', {
|
response = self.client.post(
|
||||||
'queue': self.queue.id,
|
"/api/tickets/",
|
||||||
'title': 'Test title',
|
{
|
||||||
'description': 'Test description\nMulti lines',
|
"queue": self.queue.id,
|
||||||
'submitter_email': 'test@mail.com',
|
"title": "Test title",
|
||||||
'priority': 4,
|
"description": "Test description\nMulti lines",
|
||||||
'custom_my_custom_field': 'custom value',
|
"submitter_email": "test@mail.com",
|
||||||
})
|
"priority": 4,
|
||||||
|
"custom_my_custom_field": "custom value",
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertEqual(CustomField.objects.all().first().name, "my_custom_field")
|
self.assertEqual(CustomField.objects.all().first().name, "my_custom_field")
|
||||||
self.assertEqual(TicketCustomFieldValue.objects.get(ticket=response.data['id']).value, 'custom value')
|
self.assertEqual(
|
||||||
|
TicketCustomFieldValue.objects.get(ticket=response.data["id"]).value,
|
||||||
|
"custom value",
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||||
handled_webhook_requests = requests.get('http://localhost:8124/get-past-requests')
|
handled_webhook_requests = requests.get(
|
||||||
|
"http://localhost:8124/get-past-requests"
|
||||||
|
)
|
||||||
handled_webhook_requests = handled_webhook_requests.json()
|
handled_webhook_requests = handled_webhook_requests.json()
|
||||||
self.assertTrue(len(handled_webhook_requests['new_ticket_requests']) == 1)
|
self.assertTrue(len(handled_webhook_requests["new_ticket_requests"]) == 1)
|
||||||
self.assertTrue(len(handled_webhook_requests['new_ticket_requests_1']) == 1)
|
self.assertTrue(len(handled_webhook_requests["new_ticket_requests_1"]) == 1)
|
||||||
self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 0)
|
self.assertEqual(len(handled_webhook_requests["follow_up_requests"]), 0)
|
||||||
self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["title"], "Test title")
|
self.assertEqual(
|
||||||
self.assertEqual(handled_webhook_requests['new_ticket_requests_1'][-1]["ticket"]["title"], "Test title")
|
handled_webhook_requests["new_ticket_requests"][-1]["ticket"]["title"],
|
||||||
self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["description"], "Test description\nMulti lines")
|
"Test title",
|
||||||
ticket = Ticket.objects.get(id=handled_webhook_requests["new_ticket_requests"][-1]["ticket"]["id"])
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
handled_webhook_requests["new_ticket_requests_1"][-1]["ticket"]["title"],
|
||||||
|
"Test title",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
handled_webhook_requests["new_ticket_requests"][-1]["ticket"][
|
||||||
|
"description"
|
||||||
|
],
|
||||||
|
"Test description\nMulti lines",
|
||||||
|
)
|
||||||
|
ticket = Ticket.objects.get(
|
||||||
|
id=handled_webhook_requests["new_ticket_requests"][-1]["ticket"]["id"]
|
||||||
|
)
|
||||||
ticket.set_custom_field_values()
|
ticket.set_custom_field_values()
|
||||||
serializer = TicketSerializer(ticket)
|
serializer = TicketSerializer(ticket)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
list(sorted(serializer.fields.keys())),
|
list(sorted(serializer.fields.keys())),
|
||||||
['assigned_to',
|
[
|
||||||
'attachment',
|
"assigned_to",
|
||||||
'custom_my_custom_field',
|
"attachment",
|
||||||
'description',
|
"custom_my_custom_field",
|
||||||
'due_date',
|
"description",
|
||||||
'followup_set',
|
"due_date",
|
||||||
'id',
|
"followup_set",
|
||||||
'merged_to',
|
"id",
|
||||||
'on_hold',
|
"merged_to",
|
||||||
'priority',
|
"on_hold",
|
||||||
'queue',
|
"priority",
|
||||||
'resolution',
|
"queue",
|
||||||
'status',
|
"resolution",
|
||||||
'submitter_email',
|
"status",
|
||||||
'title']
|
"submitter_email",
|
||||||
|
"title",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
serializer.data,
|
||||||
|
handled_webhook_requests["new_ticket_requests"][-1]["ticket"],
|
||||||
|
)
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/followups/",
|
||||||
|
{
|
||||||
|
"ticket": handled_webhook_requests["new_ticket_requests"][-1]["ticket"][
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"comment": "Test comment",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(serializer.data, handled_webhook_requests["new_ticket_requests"][-1]["ticket"])
|
|
||||||
response = self.client.post('/api/followups/', {
|
|
||||||
'ticket': handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["id"],
|
|
||||||
"comment": "Test comment",
|
|
||||||
})
|
|
||||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||||
handled_webhook_requests = requests.get('http://localhost:8124/get-past-requests')
|
handled_webhook_requests = requests.get(
|
||||||
|
"http://localhost:8124/get-past-requests"
|
||||||
|
)
|
||||||
handled_webhook_requests = handled_webhook_requests.json()
|
handled_webhook_requests = handled_webhook_requests.json()
|
||||||
self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 1)
|
self.assertEqual(len(handled_webhook_requests["follow_up_requests"]), 1)
|
||||||
self.assertEqual(len(handled_webhook_requests['follow_up_requests_1']), 1)
|
self.assertEqual(len(handled_webhook_requests["follow_up_requests_1"]), 1)
|
||||||
self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["followup_set"][-1]["comment"], "Test comment")
|
self.assertEqual(
|
||||||
self.assertEqual(handled_webhook_requests['follow_up_requests_1'][-1]["ticket"]["followup_set"][-1]["comment"], "Test comment")
|
handled_webhook_requests["follow_up_requests"][-1]["ticket"][
|
||||||
|
"followup_set"
|
||||||
|
][-1]["comment"],
|
||||||
|
"Test comment",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
handled_webhook_requests["follow_up_requests_1"][-1]["ticket"][
|
||||||
|
"followup_set"
|
||||||
|
][-1]["comment"],
|
||||||
|
"Test comment",
|
||||||
|
)
|
||||||
|
|
||||||
server.stop()
|
server.stop()
|
||||||
|
|
||||||
def test_create_ticket_and_followup_via_email(self):
|
def test_create_ticket_and_followup_via_email(self):
|
||||||
from .. import email
|
from .. import email
|
||||||
|
|
||||||
server = WebhookServer(('localhost', 8125), WebhookRequestHandler)
|
server = WebhookServer(("localhost", 8125), WebhookRequestHandler)
|
||||||
os.environ['HELPDESK_NEW_TICKET_WEBHOOK_URLS'] = 'http://localhost:8125/new-ticket'
|
os.environ["HELPDESK_NEW_TICKET_WEBHOOK_URLS"] = (
|
||||||
os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8125/followup'
|
"http://localhost:8125/new-ticket"
|
||||||
|
)
|
||||||
|
os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = "http://localhost:8125/followup"
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
class MockMessage(dict):
|
class MockMessage(dict):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.__dict__.update(kwargs)
|
self.__dict__.update(kwargs)
|
||||||
@ -175,13 +239,13 @@ class WebhookTest(APITestCase):
|
|||||||
return self.__dict__.get(key, default)
|
return self.__dict__.get(key, default)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
'body': "hello",
|
"body": "hello",
|
||||||
'full_body': "hello",
|
"full_body": "hello",
|
||||||
'subject': "Test subject",
|
"subject": "Test subject",
|
||||||
'queue': self.queue,
|
"queue": self.queue,
|
||||||
'sender_email': "user@example.com",
|
"sender_email": "user@example.com",
|
||||||
'priority': "1",
|
"priority": "1",
|
||||||
'files': [],
|
"files": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
message = {
|
message = {
|
||||||
@ -195,26 +259,29 @@ class WebhookTest(APITestCase):
|
|||||||
ticket_id=None,
|
ticket_id=None,
|
||||||
payload=payload,
|
payload=payload,
|
||||||
files=[],
|
files=[],
|
||||||
logger=logging.getLogger('helpdesk'),
|
logger=logging.getLogger("helpdesk"),
|
||||||
)
|
)
|
||||||
|
|
||||||
handled_webhook_requests = requests.get('http://localhost:8125/get-past-requests')
|
handled_webhook_requests = requests.get(
|
||||||
|
"http://localhost:8125/get-past-requests"
|
||||||
|
)
|
||||||
handled_webhook_requests = handled_webhook_requests.json()
|
handled_webhook_requests = handled_webhook_requests.json()
|
||||||
self.assertEqual(len(handled_webhook_requests['new_ticket_requests']), 1)
|
self.assertEqual(len(handled_webhook_requests["new_ticket_requests"]), 1)
|
||||||
self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 0)
|
self.assertEqual(len(handled_webhook_requests["follow_up_requests"]), 0)
|
||||||
|
|
||||||
ticket_id = handled_webhook_requests['new_ticket_requests'][-1]["ticket"]['id']
|
ticket_id = handled_webhook_requests["new_ticket_requests"][-1]["ticket"]["id"]
|
||||||
from .. import models
|
from .. import models
|
||||||
|
|
||||||
ticket = models.Ticket.objects.get(id=ticket_id)
|
ticket = models.Ticket.objects.get(id=ticket_id)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
'body': "hello",
|
"body": "hello",
|
||||||
'full_body': "hello",
|
"full_body": "hello",
|
||||||
'subject': f"[test-queue-{ticket_id}] Test subject",
|
"subject": f"[test-queue-{ticket_id}] Test subject",
|
||||||
'queue': self.queue,
|
"queue": self.queue,
|
||||||
'sender_email': "user@example.com",
|
"sender_email": "user@example.com",
|
||||||
'priority': "1",
|
"priority": "1",
|
||||||
'files': [],
|
"files": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
message = {
|
message = {
|
||||||
@ -228,12 +295,22 @@ class WebhookTest(APITestCase):
|
|||||||
ticket_id=ticket_id,
|
ticket_id=ticket_id,
|
||||||
payload=payload,
|
payload=payload,
|
||||||
files=[],
|
files=[],
|
||||||
logger=logging.getLogger('helpdesk'),
|
logger=logging.getLogger("helpdesk"),
|
||||||
|
)
|
||||||
|
handled_webhook_requests = requests.get(
|
||||||
|
"http://localhost:8125/get-past-requests"
|
||||||
)
|
)
|
||||||
handled_webhook_requests = requests.get('http://localhost:8125/get-past-requests')
|
|
||||||
handled_webhook_requests = handled_webhook_requests.json()
|
handled_webhook_requests = handled_webhook_requests.json()
|
||||||
self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 1)
|
self.assertEqual(len(handled_webhook_requests["follow_up_requests"]), 1)
|
||||||
self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["followup_set"][-1]["comment"], "hello")
|
self.assertEqual(
|
||||||
self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["id"], ticket_id)
|
handled_webhook_requests["follow_up_requests"][-1]["ticket"][
|
||||||
|
"followup_set"
|
||||||
|
][-1]["comment"],
|
||||||
|
"hello",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
handled_webhook_requests["follow_up_requests"][-1]["ticket"]["id"],
|
||||||
|
ticket_id,
|
||||||
|
)
|
||||||
|
|
||||||
server.stop()
|
server.stop()
|
||||||
|
@ -3,6 +3,6 @@ from django.urls import include, path
|
|||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', include('helpdesk.urls', namespace='helpdesk')),
|
path("", include("helpdesk.urls", namespace="helpdesk")),
|
||||||
path('admin/', admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
]
|
]
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
"""UItility functions facilitate making unit testing easier and less brittle."""
|
"""UItility functions facilitate making unit testing easier and less brittle."""
|
||||||
|
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import email
|
import email
|
||||||
from email import encoders
|
from email import encoders
|
||||||
@ -31,8 +30,8 @@ def strip_accents(text):
|
|||||||
:returns: The processed String.
|
:returns: The processed String.
|
||||||
:rtype: String.
|
:rtype: String.
|
||||||
"""
|
"""
|
||||||
text = unicodedata.normalize('NFD', text)
|
text = unicodedata.normalize("NFD", text)
|
||||||
text = text.encode('ascii', 'ignore')
|
text = text.encode("ascii", "ignore")
|
||||||
text = text.decode("utf-8")
|
text = text.decode("utf-8")
|
||||||
return str(text)
|
return str(text)
|
||||||
|
|
||||||
@ -48,12 +47,12 @@ def text_to_id(text):
|
|||||||
:rtype: String.
|
:rtype: String.
|
||||||
"""
|
"""
|
||||||
text = strip_accents(text.lower())
|
text = strip_accents(text.lower())
|
||||||
text = re.sub('[ ]+', '_', text)
|
text = re.sub("[ ]+", "_", text)
|
||||||
text = re.sub('[^0-9a-zA-Z_-]', '', text)
|
text = re.sub("[^0-9a-zA-Z_-]", "", text)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def get_random_string(length: int=16) -> str:
|
def get_random_string(length: int = 16) -> str:
|
||||||
return "".join(
|
return "".join(
|
||||||
[random.choice(string.ascii_letters + string.digits) for _ in range(length)]
|
[random.choice(string.ascii_letters + string.digits) for _ in range(length)]
|
||||||
)
|
)
|
||||||
@ -62,27 +61,27 @@ def get_random_string(length: int=16) -> str:
|
|||||||
def generate_random_image(image_format, array_dims):
|
def generate_random_image(image_format, array_dims):
|
||||||
"""
|
"""
|
||||||
Creates an image from a random array.
|
Creates an image from a random array.
|
||||||
|
|
||||||
:param image_format: An image format (PNG or JPEG).
|
:param image_format: An image format (PNG or JPEG).
|
||||||
:param array_dims: A tuple with array dimensions.
|
:param array_dims: A tuple with array dimensions.
|
||||||
|
|
||||||
:returns: A byte string with encoded image
|
:returns: A byte string with encoded image
|
||||||
:rtype: bytes
|
:rtype: bytes
|
||||||
"""
|
"""
|
||||||
image_bytes = randint(low=0, high=255, size=array_dims, dtype='uint8')
|
image_bytes = randint(low=0, high=255, size=array_dims, dtype="uint8")
|
||||||
io = BytesIO()
|
io = BytesIO()
|
||||||
image_pil = Image.fromarray(image_bytes)
|
image_pil = Image.fromarray(image_bytes)
|
||||||
image_pil.save(io, image_format, subsampling=0, quality=100)
|
image_pil.save(io, image_format, subsampling=0, quality=100)
|
||||||
return io.getvalue()
|
return io.getvalue()
|
||||||
|
|
||||||
|
|
||||||
def get_random_image(image_format: str="PNG", size: int=5):
|
def get_random_image(image_format: str = "PNG", size: int = 5):
|
||||||
"""
|
"""
|
||||||
Returns a random image.
|
Returns a random image.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
image_format: An image format (PNG or JPEG).
|
image_format: An image format (PNG or JPEG).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A string with encoded image
|
A string with encoded image
|
||||||
"""
|
"""
|
||||||
@ -92,120 +91,186 @@ def get_random_image(image_format: str="PNG", size: int=5):
|
|||||||
def get_fake(provider: str, locale: str = "en_US", min_length: int = 5) -> Any:
|
def get_fake(provider: str, locale: str = "en_US", min_length: int = 5) -> Any:
|
||||||
"""
|
"""
|
||||||
Generates a random string, float, integer etc based on provider
|
Generates a random string, float, integer etc based on provider
|
||||||
Provider can be "text', 'sentence', "word"
|
Provider can be "text', 'sentence', "word"
|
||||||
e.g. `get_fake('name')` ==> 'Buzz Aldrin'
|
e.g. `get_fake('name')` ==> 'Buzz Aldrin'
|
||||||
"""
|
"""
|
||||||
string = factory.Faker(provider).evaluate({}, None, {'locale': locale,})
|
string = factory.Faker(provider).evaluate(
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"locale": locale,
|
||||||
|
},
|
||||||
|
)
|
||||||
while len(string) < min_length:
|
while len(string) < min_length:
|
||||||
string += factory.Faker(provider).evaluate({}, None, {'locale': locale,})
|
string += factory.Faker(provider).evaluate(
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"locale": locale,
|
||||||
|
},
|
||||||
|
)
|
||||||
return string
|
return string
|
||||||
|
|
||||||
|
|
||||||
def get_fake_html(locale: str = "en_US", wrap_in_body_tag=True) -> Any:
|
def get_fake_html(locale: str = "en_US", wrap_in_body_tag=True) -> Any:
|
||||||
"""
|
"""
|
||||||
Generates a random string, float, integer etc based on provider
|
Generates a random string, float, integer etc based on provider
|
||||||
Provider can be "text', 'sentence',
|
Provider can be "text', 'sentence',
|
||||||
e.g. `get_fake('name')` ==> 'Buzz Aldrin'
|
e.g. `get_fake('name')` ==> 'Buzz Aldrin'
|
||||||
"""
|
"""
|
||||||
html = factory.Faker("sentence").evaluate({}, None, {'locale': locale,})
|
html = factory.Faker("sentence").evaluate(
|
||||||
for _ in range(0,4):
|
{},
|
||||||
html += "<li>" + factory.Faker("sentence").evaluate({}, None, {'locale': locale,}) + "</li>"
|
None,
|
||||||
for _ in range(0,4):
|
{
|
||||||
html += "<p>" + factory.Faker("text").evaluate({}, None, {'locale': locale,})
|
"locale": locale,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
for _ in range(0, 4):
|
||||||
|
html += (
|
||||||
|
"<li>"
|
||||||
|
+ factory.Faker("sentence").evaluate(
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"locale": locale,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
+ "</li>"
|
||||||
|
)
|
||||||
|
for _ in range(0, 4):
|
||||||
|
html += "<p>" + factory.Faker("text").evaluate(
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"locale": locale,
|
||||||
|
},
|
||||||
|
)
|
||||||
return f"<body>{html}</body>" if wrap_in_body_tag else html
|
return f"<body>{html}</body>" if wrap_in_body_tag else html
|
||||||
|
|
||||||
|
|
||||||
def generate_email_address(
|
def generate_email_address(
|
||||||
locale: str="en_US",
|
locale: str = "en_US",
|
||||||
use_short_email: bool=False,
|
use_short_email: bool = False,
|
||||||
real_name_format: Optional[str]="{last_name}, {first_name}",
|
real_name_format: Optional[str] = "{last_name}, {first_name}",
|
||||||
last_name_override: Optional[str]=None) -> Tuple[str, str, str, str]:
|
last_name_override: Optional[str] = None,
|
||||||
'''
|
) -> Tuple[str, str, str, str]:
|
||||||
|
"""
|
||||||
Generate an RFC 2822 email address
|
Generate an RFC 2822 email address
|
||||||
|
|
||||||
:param locale: change this to generate locale specific names
|
:param locale: change this to generate locale specific names
|
||||||
:param use_short_email: defaults to false. If true then does not include real name in email address
|
:param use_short_email: defaults to false. If true then does not include real name in email address
|
||||||
:param real_name_format: pass a different format if different than "{last_name}, {first_name}"
|
:param real_name_format: pass a different format if different than "{last_name}, {first_name}"
|
||||||
:param last_name_override: override the fake name if you want some special characters in the last name
|
:param last_name_override: override the fake name if you want some special characters in the last name
|
||||||
:returns <RFC2822 formatted email for header>, <short email address>, <first name>, <last_name
|
:returns <RFC2822 formatted email for header>, <short email address>, <first name>, <last_name
|
||||||
'''
|
"""
|
||||||
fake = faker.Faker(locale=locale)
|
fake = faker.Faker(locale=locale)
|
||||||
first_name = fake.first_name()
|
first_name = fake.first_name()
|
||||||
last_name = last_name_override or fake.last_name()
|
last_name = last_name_override or fake.last_name()
|
||||||
real_name = None if use_short_email else real_name_format.format(first_name=first_name, last_name=last_name)
|
real_name = (
|
||||||
|
None
|
||||||
|
if use_short_email
|
||||||
|
else real_name_format.format(first_name=first_name, last_name=last_name)
|
||||||
|
)
|
||||||
# Add a random string to ensure we do not generate a real domain name
|
# Add a random string to ensure we do not generate a real domain name
|
||||||
email_address = "{}.{}@{}".format(
|
email_address = "{}.{}@{}".format(
|
||||||
first_name.replace(' ', '').encode("ascii", "ignore").lower().decode(),
|
first_name.replace(" ", "").encode("ascii", "ignore").lower().decode(),
|
||||||
last_name.replace(' ', '').encode("ascii", "ignore").lower().decode(),
|
last_name.replace(" ", "").encode("ascii", "ignore").lower().decode(),
|
||||||
get_random_string(5) + fake.domain_name()
|
get_random_string(5) + fake.domain_name(),
|
||||||
)
|
)
|
||||||
# format email address for RFC 2822 and return
|
# format email address for RFC 2822 and return
|
||||||
return email.utils.formataddr((real_name, email_address)), email_address, first_name, last_name
|
return (
|
||||||
|
email.utils.formataddr((real_name, email_address)),
|
||||||
|
email_address,
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
)
|
||||||
|
|
||||||
def generate_file_mime_part(locale: str="en_US",filename: str = None, content: str = None) -> Message:
|
|
||||||
|
def generate_file_mime_part(
|
||||||
|
locale: str = "en_US", filename: str = None, content: str = None
|
||||||
|
) -> Message:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
:param locale: change this to generate locale specific file name and attachment content
|
:param locale: change this to generate locale specific file name and attachment content
|
||||||
:param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated
|
:param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated
|
||||||
:param content: pass a string value if you want have specific content otherwise a random string will be generated
|
:param content: pass a string value if you want have specific content otherwise a random string will be generated
|
||||||
"""
|
"""
|
||||||
part = MIMEBase('application', 'octet-stream')
|
part = MIMEBase("application", "octet-stream")
|
||||||
part.set_payload(get_fake("text", locale=locale, min_length=1024) if content is None else content)
|
part.set_payload(
|
||||||
|
get_fake("text", locale=locale, min_length=1024) if content is None else content
|
||||||
|
)
|
||||||
encoders.encode_base64(part)
|
encoders.encode_base64(part)
|
||||||
if not filename:
|
if not filename:
|
||||||
filename = get_fake("word", locale=locale, min_length=8) + ".txt"
|
filename = get_fake("word", locale=locale, min_length=8) + ".txt"
|
||||||
part.add_header('Content-Disposition', "attachment; filename=%s" % filename)
|
part.add_header("Content-Disposition", "attachment; filename=%s" % filename)
|
||||||
return part
|
return part
|
||||||
|
|
||||||
def generate_executable_mime_part(locale: str="en_US",filename: str = None, content: str = None) -> Message:
|
|
||||||
|
def generate_executable_mime_part(
|
||||||
|
locale: str = "en_US", filename: str = None, content: str = None
|
||||||
|
) -> Message:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
:param locale: change this to generate locale specific file name and attachment content
|
:param locale: change this to generate locale specific file name and attachment content
|
||||||
:param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated
|
:param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated
|
||||||
:param content: pass a string value if you want have specific content otherwise a random string will be generated
|
:param content: pass a string value if you want have specific content otherwise a random string will be generated
|
||||||
"""
|
"""
|
||||||
part = MIMEBase('application', 'vnd.microsoft.portable-executable')
|
part = MIMEBase("application", "vnd.microsoft.portable-executable")
|
||||||
part.set_payload(get_fake("text", locale=locale, min_length=1024) if content is None else content)
|
part.set_payload(
|
||||||
|
get_fake("text", locale=locale, min_length=1024) if content is None else content
|
||||||
|
)
|
||||||
encoders.encode_base64(part)
|
encoders.encode_base64(part)
|
||||||
if not filename:
|
if not filename:
|
||||||
filename = get_fake("word", locale=locale, min_length=8) + ".exe"
|
filename = get_fake("word", locale=locale, min_length=8) + ".exe"
|
||||||
part.add_header('Content-Disposition', "attachment; filename=%s" % filename)
|
part.add_header("Content-Disposition", "attachment; filename=%s" % filename)
|
||||||
return part
|
return part
|
||||||
|
|
||||||
def generate_image_mime_part(locale: str="en_US",imagename: str = None, disposition_primary_type: str = "attachment") -> Message:
|
|
||||||
|
def generate_image_mime_part(
|
||||||
|
locale: str = "en_US",
|
||||||
|
imagename: str = None,
|
||||||
|
disposition_primary_type: str = "attachment",
|
||||||
|
) -> Message:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
:param locale: change this to generate locale specific file name and attachment content
|
:param locale: change this to generate locale specific file name and attachment content
|
||||||
:param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated
|
:param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated
|
||||||
"""
|
"""
|
||||||
part = MIMEImage(generate_random_image(image_format="JPEG", array_dims=(200, 200)))
|
part = MIMEImage(generate_random_image(image_format="JPEG", array_dims=(200, 200)))
|
||||||
#part.set_payload(get_fake("text", locale=locale, min_length=1024))
|
# part.set_payload(get_fake("text", locale=locale, min_length=1024))
|
||||||
encoders.encode_base64(part)
|
encoders.encode_base64(part)
|
||||||
if not imagename:
|
if not imagename:
|
||||||
imagename = get_fake("word", locale=locale, min_length=8) + ".jpg"
|
imagename = get_fake("word", locale=locale, min_length=8) + ".jpg"
|
||||||
part.add_header('Content-Disposition', disposition_primary_type + "; filename= %s" % imagename)
|
part.add_header(
|
||||||
|
"Content-Disposition", disposition_primary_type + "; filename= %s" % imagename
|
||||||
|
)
|
||||||
return part
|
return part
|
||||||
|
|
||||||
def generate_email_list(address_cnt: int = 3,
|
|
||||||
locale: str="en_US",
|
def generate_email_list(
|
||||||
use_short_email: bool=False
|
address_cnt: int = 3, locale: str = "en_US", use_short_email: bool = False
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Generates a list of email addresses formatted for email headers on a Mime part
|
Generates a list of email addresses formatted for email headers on a Mime part
|
||||||
|
|
||||||
:param address_cnt: the number of email addresses to string together
|
:param address_cnt: the number of email addresses to string together
|
||||||
:param locale: change this to generate locale specific "real names" and subject
|
:param locale: change this to generate locale specific "real names" and subject
|
||||||
:param use_short_email: produces a email address without "real name" if True
|
:param use_short_email: produces a email address without "real name" if True
|
||||||
|
|
||||||
"""
|
"""
|
||||||
email_address_list = [generate_email_address(locale, use_short_email=use_short_email)[0] for _ in range(0, address_cnt)]
|
email_address_list = [
|
||||||
|
generate_email_address(locale, use_short_email=use_short_email)[0]
|
||||||
|
for _ in range(0, address_cnt)
|
||||||
|
]
|
||||||
return ",".join(email_address_list)
|
return ",".join(email_address_list)
|
||||||
|
|
||||||
def add_simple_email_headers(message: Message, locale: str="en_US",
|
|
||||||
use_short_email: bool=False
|
def add_simple_email_headers(
|
||||||
) -> typing.Tuple[typing.Tuple[str, str], typing.Tuple[str, str]]:
|
message: Message, locale: str = "en_US", use_short_email: bool = False
|
||||||
|
) -> typing.Tuple[typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||||
"""
|
"""
|
||||||
Adds the key email headers to a Mime part
|
Adds the key email headers to a Mime part
|
||||||
|
|
||||||
:param message: the Mime part to add headers to
|
:param message: the Mime part to add headers to
|
||||||
:param locale: change this to generate locale specific "real names" and subject
|
:param locale: change this to generate locale specific "real names" and subject
|
||||||
:param use_short_email: produces a "To" or "From" that is only the email address if True
|
:param use_short_email: produces a "To" or "From" that is only the email address if True
|
||||||
@ -213,18 +278,20 @@ def add_simple_email_headers(message: Message, locale: str="en_US",
|
|||||||
"""
|
"""
|
||||||
to_meta = generate_email_address(locale, use_short_email=use_short_email)
|
to_meta = generate_email_address(locale, use_short_email=use_short_email)
|
||||||
from_meta = generate_email_address(locale, use_short_email=use_short_email)
|
from_meta = generate_email_address(locale, use_short_email=use_short_email)
|
||||||
|
|
||||||
message['Subject'] = get_fake("sentence", locale=locale)
|
message["Subject"] = get_fake("sentence", locale=locale)
|
||||||
message['From'] = from_meta[0]
|
message["From"] = from_meta[0]
|
||||||
message['To'] = to_meta[0]
|
message["To"] = to_meta[0]
|
||||||
return from_meta, to_meta
|
return from_meta, to_meta
|
||||||
|
|
||||||
def generate_mime_part(locale: str="en_US",
|
|
||||||
part_type: str="plain",
|
def generate_mime_part(
|
||||||
) -> typing.Optional[Message]:
|
locale: str = "en_US",
|
||||||
|
part_type: str = "plain",
|
||||||
|
) -> typing.Optional[Message]:
|
||||||
"""
|
"""
|
||||||
Generates amime part of the sepecified type
|
Generates amime part of the sepecified type
|
||||||
|
|
||||||
:param locale: change this to generate locale specific strings
|
:param locale: change this to generate locale specific strings
|
||||||
:param text_type: options are plain, html, image (attachment), file (attachment)
|
:param text_type: options are plain, html, image (attachment), file (attachment)
|
||||||
"""
|
"""
|
||||||
@ -244,43 +311,53 @@ def generate_mime_part(locale: str="en_US",
|
|||||||
raise Exception("Mime part not implemented: " + part_type)
|
raise Exception("Mime part not implemented: " + part_type)
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
def generate_multipart_email(locale: str="en_US",
|
|
||||||
type_list: typing.List[str]=["plain", "html", "image"],
|
def generate_multipart_email(
|
||||||
sub_type: str = None,
|
locale: str = "en_US",
|
||||||
use_short_email: bool=False
|
type_list: typing.List[str] = ["plain", "html", "image"],
|
||||||
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
|
sub_type: str = None,
|
||||||
|
use_short_email: bool = False,
|
||||||
|
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||||
"""
|
"""
|
||||||
Generates an email including headers with the defined multiparts
|
Generates an email including headers with the defined multiparts
|
||||||
|
|
||||||
:param locale:
|
:param locale:
|
||||||
:param type_list: options are plain, html, image (attachment), file (attachment), and executable (.exe attachment)
|
:param type_list: options are plain, html, image (attachment), file (attachment), and executable (.exe attachment)
|
||||||
:param sub_type: multipart sub type that defaults to "mixed" if not specified
|
:param sub_type: multipart sub type that defaults to "mixed" if not specified
|
||||||
:param use_short_email: produces a "To" or "From" that is only the email address if True
|
:param use_short_email: produces a "To" or "From" that is only the email address if True
|
||||||
"""
|
"""
|
||||||
msg = MIMEMultipart(sub_type) if sub_type else MIMEMultipart()
|
msg = MIMEMultipart(sub_type) if sub_type else MIMEMultipart()
|
||||||
for part_type in type_list:
|
for part_type in type_list:
|
||||||
msg.attach(generate_mime_part(locale=locale, part_type=part_type))
|
msg.attach(generate_mime_part(locale=locale, part_type=part_type))
|
||||||
from_meta, to_meta = add_simple_email_headers(msg, locale=locale, use_short_email=use_short_email)
|
from_meta, to_meta = add_simple_email_headers(
|
||||||
|
msg, locale=locale, use_short_email=use_short_email
|
||||||
|
)
|
||||||
return msg, from_meta, to_meta
|
return msg, from_meta, to_meta
|
||||||
|
|
||||||
def generate_text_email(locale: str="en_US",
|
|
||||||
use_short_email: bool=False
|
def generate_text_email(
|
||||||
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
|
locale: str = "en_US", use_short_email: bool = False
|
||||||
|
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||||
"""
|
"""
|
||||||
Generates an email including headers
|
Generates an email including headers
|
||||||
"""
|
"""
|
||||||
body = get_fake("text", locale=locale, min_length=1024)
|
body = get_fake("text", locale=locale, min_length=1024)
|
||||||
msg = MIMEText(body)
|
msg = MIMEText(body)
|
||||||
from_meta, to_meta = add_simple_email_headers(msg, locale=locale, use_short_email=use_short_email)
|
from_meta, to_meta = add_simple_email_headers(
|
||||||
|
msg, locale=locale, use_short_email=use_short_email
|
||||||
|
)
|
||||||
return msg, from_meta, to_meta
|
return msg, from_meta, to_meta
|
||||||
|
|
||||||
def generate_html_email(locale: str="en_US",
|
|
||||||
use_short_email: bool=False
|
def generate_html_email(
|
||||||
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
|
locale: str = "en_US", use_short_email: bool = False
|
||||||
|
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||||
"""
|
"""
|
||||||
Generates an email including headers
|
Generates an email including headers
|
||||||
"""
|
"""
|
||||||
body = get_fake_html(locale=locale)
|
body = get_fake_html(locale=locale)
|
||||||
msg = MIMEText(body)
|
msg = MIMEText(body)
|
||||||
from_meta, to_meta = add_simple_email_headers(msg, locale=locale, use_short_email=use_short_email)
|
from_meta, to_meta = add_simple_email_headers(
|
||||||
|
msg, locale=locale, use_short_email=use_short_email
|
||||||
|
)
|
||||||
return msg, from_meta, to_meta
|
return msg, from_meta, to_meta
|
||||||
|
@ -20,15 +20,15 @@ from helpdesk.signals import update_ticket_done
|
|||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
def add_staff_subscription(
|
|
||||||
user: User,
|
def add_staff_subscription(user: User, ticket: Ticket) -> None:
|
||||||
ticket: Ticket
|
|
||||||
) -> None:
|
|
||||||
"""Auto subscribe the staff member if that's what the settigs say and the
|
"""Auto subscribe the staff member if that's what the settigs say and the
|
||||||
user is authenticated and a staff member"""
|
user is authenticated and a staff member"""
|
||||||
if helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE \
|
if (
|
||||||
and user.is_authenticated \
|
helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE
|
||||||
and return_ticketccstring_and_show_subscribe(user, ticket)[1]:
|
and user.is_authenticated
|
||||||
|
and return_ticketccstring_and_show_subscribe(user, ticket)[1]
|
||||||
|
):
|
||||||
subscribe_to_ticket_updates(ticket, user)
|
subscribe_to_ticket_updates(ticket, user)
|
||||||
|
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ def return_ticketccstring_and_show_subscribe(user, ticket):
|
|||||||
strings_to_check.append(username)
|
strings_to_check.append(username)
|
||||||
strings_to_check.append(useremail)
|
strings_to_check.append(useremail)
|
||||||
|
|
||||||
ticketcc_string = ''
|
ticketcc_string = ""
|
||||||
all_ticketcc = ticket.ticketcc_set.all()
|
all_ticketcc = ticket.ticketcc_set.all()
|
||||||
counter_all_ticketcc = len(all_ticketcc) - 1
|
counter_all_ticketcc = len(all_ticketcc) - 1
|
||||||
show_subscribe = True
|
show_subscribe = True
|
||||||
@ -53,7 +53,7 @@ def return_ticketccstring_and_show_subscribe(user, ticket):
|
|||||||
ticketcc_this_entry = str(ticketcc.display)
|
ticketcc_this_entry = str(ticketcc.display)
|
||||||
ticketcc_string += ticketcc_this_entry
|
ticketcc_string += ticketcc_this_entry
|
||||||
if i < counter_all_ticketcc:
|
if i < counter_all_ticketcc:
|
||||||
ticketcc_string += ', '
|
ticketcc_string += ", "
|
||||||
if strings_to_check.__contains__(ticketcc_this_entry.upper()):
|
if strings_to_check.__contains__(ticketcc_this_entry.upper()):
|
||||||
show_subscribe = False
|
show_subscribe = False
|
||||||
|
|
||||||
@ -64,18 +64,19 @@ def return_ticketccstring_and_show_subscribe(user, ticket):
|
|||||||
submitter_email = ticket.submitter_email.upper()
|
submitter_email = ticket.submitter_email.upper()
|
||||||
strings_to_check.append(submitter_email)
|
strings_to_check.append(submitter_email)
|
||||||
strings_to_check.append(assignedto_username)
|
strings_to_check.append(assignedto_username)
|
||||||
if strings_to_check.__contains__(username) or strings_to_check.__contains__(useremail):
|
if strings_to_check.__contains__(username) or strings_to_check.__contains__(
|
||||||
|
useremail
|
||||||
|
):
|
||||||
show_subscribe = False
|
show_subscribe = False
|
||||||
|
|
||||||
return ticketcc_string, show_subscribe
|
return ticketcc_string, show_subscribe
|
||||||
|
|
||||||
|
|
||||||
def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, can_update=False):
|
def subscribe_to_ticket_updates(
|
||||||
|
ticket, user=None, email=None, can_view=True, can_update=False
|
||||||
|
):
|
||||||
if ticket is not None:
|
if ticket is not None:
|
||||||
|
queryset = TicketCC.objects.filter(ticket=ticket, user=user, email=email)
|
||||||
queryset = TicketCC.objects.filter(
|
|
||||||
ticket=ticket, user=user, email=email)
|
|
||||||
|
|
||||||
# Don't create duplicate entries for subscribers
|
# Don't create duplicate entries for subscribers
|
||||||
if queryset.count() > 0:
|
if queryset.count() > 0:
|
||||||
@ -83,21 +84,19 @@ def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, ca
|
|||||||
|
|
||||||
if user is None and len(email) < 5:
|
if user is None and len(email) < 5:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('When you add somebody on Cc, you must provide either a User or a valid email. Email: %s' % email)
|
_(
|
||||||
|
"When you add somebody on Cc, you must provide either a User or a valid email. Email: %s"
|
||||||
|
% email
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return ticket.ticketcc_set.create(
|
return ticket.ticketcc_set.create(
|
||||||
user=user,
|
user=user, email=email, can_view=can_view, can_update=can_update
|
||||||
email=email,
|
|
||||||
can_view=can_view,
|
|
||||||
can_update=can_update
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_and_set_ticket_status(
|
def get_and_set_ticket_status(
|
||||||
new_status: int,
|
new_status: int, ticket: Ticket, follow_up: FollowUp
|
||||||
ticket: Ticket,
|
|
||||||
follow_up: FollowUp
|
|
||||||
) -> typing.Tuple[str, int]:
|
) -> typing.Tuple[str, int]:
|
||||||
"""Performs comparision on previous status to new status,
|
"""Performs comparision on previous status to new status,
|
||||||
updating the title as required.
|
updating the title as required.
|
||||||
@ -112,15 +111,15 @@ def get_and_set_ticket_status(
|
|||||||
ticket.save()
|
ticket.save()
|
||||||
follow_up.new_status = new_status
|
follow_up.new_status = new_status
|
||||||
if follow_up.title:
|
if follow_up.title:
|
||||||
follow_up.title += ' and %s' % ticket.get_status_display()
|
follow_up.title += " and %s" % ticket.get_status_display()
|
||||||
else:
|
else:
|
||||||
follow_up.title = '%s' % ticket.get_status_display()
|
follow_up.title = "%s" % ticket.get_status_display()
|
||||||
|
|
||||||
if not follow_up.title:
|
if not follow_up.title:
|
||||||
if follow_up.comment:
|
if follow_up.comment:
|
||||||
follow_up.title = _('Comment')
|
follow_up.title = _("Comment")
|
||||||
else:
|
else:
|
||||||
follow_up.title = _('Updated')
|
follow_up.title = _("Updated")
|
||||||
|
|
||||||
follow_up.save()
|
follow_up.save()
|
||||||
return old_status_str, old_status
|
return old_status_str, old_status
|
||||||
@ -132,80 +131,76 @@ def update_messages_sent_to_by_public_and_status(
|
|||||||
follow_up: FollowUp,
|
follow_up: FollowUp,
|
||||||
context: str,
|
context: str,
|
||||||
messages_sent_to: typing.Set[str],
|
messages_sent_to: typing.Set[str],
|
||||||
files: typing.List[typing.Tuple[str, str]]
|
files: typing.List[typing.Tuple[str, str]],
|
||||||
) -> Ticket:
|
) -> Ticket:
|
||||||
"""Sets the status of the ticket"""
|
"""Sets the status of the ticket"""
|
||||||
if public and (
|
if public and (
|
||||||
follow_up.comment or (
|
follow_up.comment
|
||||||
follow_up.new_status in (
|
or (follow_up.new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS))
|
||||||
Ticket.RESOLVED_STATUS,
|
|
||||||
Ticket.CLOSED_STATUS
|
|
||||||
)
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
if follow_up.new_status == Ticket.RESOLVED_STATUS:
|
if follow_up.new_status == Ticket.RESOLVED_STATUS:
|
||||||
template = 'resolved_'
|
template = "resolved_"
|
||||||
elif follow_up.new_status == Ticket.CLOSED_STATUS:
|
elif follow_up.new_status == Ticket.CLOSED_STATUS:
|
||||||
template = 'closed_'
|
template = "closed_"
|
||||||
else:
|
else:
|
||||||
template = 'updated_'
|
template = "updated_"
|
||||||
|
|
||||||
roles = {
|
roles = {
|
||||||
'submitter': (template + 'submitter', context),
|
"submitter": (template + "submitter", context),
|
||||||
'ticket_cc': (template + 'cc', context),
|
"ticket_cc": (template + "cc", context),
|
||||||
}
|
}
|
||||||
if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change:
|
if (
|
||||||
roles['assigned_to'] = (template + 'cc', context)
|
ticket.assigned_to
|
||||||
|
and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change
|
||||||
|
):
|
||||||
|
roles["assigned_to"] = (template + "cc", context)
|
||||||
messages_sent_to.update(
|
messages_sent_to.update(
|
||||||
ticket.send(
|
ticket.send(
|
||||||
roles,
|
roles, dont_send_to=messages_sent_to, fail_silently=True, files=files
|
||||||
dont_send_to=messages_sent_to,
|
|
||||||
fail_silently=True,
|
|
||||||
files=files
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return ticket
|
return ticket
|
||||||
|
|
||||||
|
|
||||||
def get_template_staff_and_template_cc(
|
def get_template_staff_and_template_cc(
|
||||||
reassigned, follow_up: FollowUp
|
reassigned, follow_up: FollowUp
|
||||||
) -> typing.Tuple[str, str]:
|
) -> typing.Tuple[str, str]:
|
||||||
if reassigned:
|
if reassigned:
|
||||||
template_staff = 'assigned_owner'
|
template_staff = "assigned_owner"
|
||||||
elif follow_up.new_status == Ticket.RESOLVED_STATUS:
|
elif follow_up.new_status == Ticket.RESOLVED_STATUS:
|
||||||
template_staff = 'resolved_owner'
|
template_staff = "resolved_owner"
|
||||||
elif follow_up.new_status == Ticket.CLOSED_STATUS:
|
elif follow_up.new_status == Ticket.CLOSED_STATUS:
|
||||||
template_staff = 'closed_owner'
|
template_staff = "closed_owner"
|
||||||
else:
|
else:
|
||||||
template_staff = 'updated_owner'
|
template_staff = "updated_owner"
|
||||||
if reassigned:
|
if reassigned:
|
||||||
template_cc = 'assigned_cc'
|
template_cc = "assigned_cc"
|
||||||
elif follow_up.new_status == Ticket.RESOLVED_STATUS:
|
elif follow_up.new_status == Ticket.RESOLVED_STATUS:
|
||||||
template_cc = 'resolved_cc'
|
template_cc = "resolved_cc"
|
||||||
elif follow_up.new_status == Ticket.CLOSED_STATUS:
|
elif follow_up.new_status == Ticket.CLOSED_STATUS:
|
||||||
template_cc = 'closed_cc'
|
template_cc = "closed_cc"
|
||||||
else:
|
else:
|
||||||
template_cc = 'updated_cc'
|
template_cc = "updated_cc"
|
||||||
|
|
||||||
return template_staff, template_cc
|
return template_staff, template_cc
|
||||||
|
|
||||||
|
|
||||||
def update_ticket(
|
def update_ticket(
|
||||||
user,
|
user,
|
||||||
ticket,
|
ticket,
|
||||||
title=None,
|
title=None,
|
||||||
comment="",
|
comment="",
|
||||||
files=None,
|
files=None,
|
||||||
public=False,
|
public=False,
|
||||||
owner=-1,
|
owner=-1,
|
||||||
priority=-1,
|
priority=-1,
|
||||||
queue=-1,
|
queue=-1,
|
||||||
new_status=None,
|
new_status=None,
|
||||||
time_spent=None,
|
time_spent=None,
|
||||||
due_date=None,
|
due_date=None,
|
||||||
new_checklists=None,
|
new_checklists=None,
|
||||||
message_id=None,
|
message_id=None,
|
||||||
customfields_form=None,
|
customfields_form=None,
|
||||||
):
|
):
|
||||||
# We need to allow the 'ticket' and 'queue' contexts to be applied to the
|
# We need to allow the 'ticket' and 'queue' contexts to be applied to the
|
||||||
# comment.
|
# comment.
|
||||||
@ -222,25 +217,31 @@ def update_ticket(
|
|||||||
new_checklists = {}
|
new_checklists = {}
|
||||||
|
|
||||||
from django.template import engines
|
from django.template import engines
|
||||||
template_func = engines['django'].from_string
|
|
||||||
|
template_func = engines["django"].from_string
|
||||||
# this prevents system from trying to render any template tags
|
# this prevents system from trying to render any template tags
|
||||||
# broken into two stages to prevent changes from first replace being themselves
|
# broken into two stages to prevent changes from first replace being themselves
|
||||||
# changed by the second replace due to conflicting syntax
|
# changed by the second replace due to conflicting syntax
|
||||||
comment = comment.replace(
|
comment = comment.replace("{%", "X-HELPDESK-COMMENT-VERBATIM").replace(
|
||||||
'{%', 'X-HELPDESK-COMMENT-VERBATIM').replace('%}', 'X-HELPDESK-COMMENT-ENDVERBATIM')
|
"%}", "X-HELPDESK-COMMENT-ENDVERBATIM"
|
||||||
comment = comment.replace(
|
|
||||||
'X-HELPDESK-COMMENT-VERBATIM', '{% verbatim %}{%'
|
|
||||||
).replace(
|
|
||||||
'X-HELPDESK-COMMENT-ENDVERBATIM', '%}{% endverbatim %}'
|
|
||||||
)
|
)
|
||||||
|
comment = comment.replace(
|
||||||
|
"X-HELPDESK-COMMENT-VERBATIM", "{% verbatim %}{%"
|
||||||
|
).replace("X-HELPDESK-COMMENT-ENDVERBATIM", "%}{% endverbatim %}")
|
||||||
# render the neutralized template
|
# render the neutralized template
|
||||||
comment = template_func(comment).render(context)
|
comment = template_func(comment).render(context)
|
||||||
|
|
||||||
if owner == -1 and ticket.assigned_to:
|
if owner == -1 and ticket.assigned_to:
|
||||||
owner = ticket.assigned_to.id
|
owner = ticket.assigned_to.id
|
||||||
|
|
||||||
f = FollowUp(ticket=ticket, date=timezone.now(), comment=comment,
|
f = FollowUp(
|
||||||
time_spent=time_spent, message_id=message_id, title=title)
|
ticket=ticket,
|
||||||
|
date=timezone.now(),
|
||||||
|
comment=comment,
|
||||||
|
time_spent=time_spent,
|
||||||
|
message_id=message_id,
|
||||||
|
title=title,
|
||||||
|
)
|
||||||
|
|
||||||
if is_helpdesk_staff(user):
|
if is_helpdesk_staff(user):
|
||||||
f.user = user
|
f.user = user
|
||||||
@ -251,16 +252,19 @@ def update_ticket(
|
|||||||
|
|
||||||
old_owner = ticket.assigned_to
|
old_owner = ticket.assigned_to
|
||||||
if owner != -1:
|
if owner != -1:
|
||||||
if owner != 0 and ((ticket.assigned_to and owner != ticket.assigned_to.id) or not ticket.assigned_to):
|
if owner != 0 and (
|
||||||
|
(ticket.assigned_to and owner != ticket.assigned_to.id)
|
||||||
|
or not ticket.assigned_to
|
||||||
|
):
|
||||||
new_user = User.objects.get(id=owner)
|
new_user = User.objects.get(id=owner)
|
||||||
f.title = _('Assigned to %(username)s') % {
|
f.title = _("Assigned to %(username)s") % {
|
||||||
'username': new_user.get_username(),
|
"username": new_user.get_username(),
|
||||||
}
|
}
|
||||||
ticket.assigned_to = new_user
|
ticket.assigned_to = new_user
|
||||||
reassigned = True
|
reassigned = True
|
||||||
# user changed owner to 'unassign'
|
# user changed owner to 'unassign'
|
||||||
elif owner == 0 and ticket.assigned_to is not None:
|
elif owner == 0 and ticket.assigned_to is not None:
|
||||||
f.title = _('Unassigned')
|
f.title = _("Unassigned")
|
||||||
ticket.assigned_to = None
|
ticket.assigned_to = None
|
||||||
|
|
||||||
old_status_str, old_status = get_and_set_ticket_status(new_status, ticket, f)
|
old_status_str, old_status = get_and_set_ticket_status(new_status, ticket, f)
|
||||||
@ -269,7 +273,7 @@ def update_ticket(
|
|||||||
|
|
||||||
if title and title != ticket.title:
|
if title and title != ticket.title:
|
||||||
c = f.ticketchange_set.create(
|
c = f.ticketchange_set.create(
|
||||||
field=_('Title'),
|
field=_("Title"),
|
||||||
old_value=ticket.title,
|
old_value=ticket.title,
|
||||||
new_value=title,
|
new_value=title,
|
||||||
)
|
)
|
||||||
@ -277,21 +281,21 @@ def update_ticket(
|
|||||||
|
|
||||||
if new_status != old_status:
|
if new_status != old_status:
|
||||||
c = f.ticketchange_set.create(
|
c = f.ticketchange_set.create(
|
||||||
field=_('Status'),
|
field=_("Status"),
|
||||||
old_value=old_status_str,
|
old_value=old_status_str,
|
||||||
new_value=ticket.get_status_display(),
|
new_value=ticket.get_status_display(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if ticket.assigned_to != old_owner:
|
if ticket.assigned_to != old_owner:
|
||||||
c = f.ticketchange_set.create(
|
c = f.ticketchange_set.create(
|
||||||
field=_('Owner'),
|
field=_("Owner"),
|
||||||
old_value=old_owner,
|
old_value=old_owner,
|
||||||
new_value=ticket.assigned_to,
|
new_value=ticket.assigned_to,
|
||||||
)
|
)
|
||||||
|
|
||||||
if priority != ticket.priority:
|
if priority != ticket.priority:
|
||||||
c = f.ticketchange_set.create(
|
c = f.ticketchange_set.create(
|
||||||
field=_('Priority'),
|
field=_("Priority"),
|
||||||
old_value=ticket.priority,
|
old_value=ticket.priority,
|
||||||
new_value=priority,
|
new_value=priority,
|
||||||
)
|
)
|
||||||
@ -299,7 +303,7 @@ def update_ticket(
|
|||||||
|
|
||||||
if queue != ticket.queue.id:
|
if queue != ticket.queue.id:
|
||||||
c = f.ticketchange_set.create(
|
c = f.ticketchange_set.create(
|
||||||
field=_('Queue'),
|
field=_("Queue"),
|
||||||
old_value=ticket.queue.id,
|
old_value=ticket.queue.id,
|
||||||
new_value=queue,
|
new_value=queue,
|
||||||
)
|
)
|
||||||
@ -307,16 +311,16 @@ def update_ticket(
|
|||||||
|
|
||||||
if due_date != ticket.due_date:
|
if due_date != ticket.due_date:
|
||||||
c = f.ticketchange_set.create(
|
c = f.ticketchange_set.create(
|
||||||
field=_('Due on'),
|
field=_("Due on"),
|
||||||
old_value=ticket.due_date,
|
old_value=ticket.due_date,
|
||||||
new_value=due_date,
|
new_value=due_date,
|
||||||
)
|
)
|
||||||
ticket.due_date = due_date
|
ticket.due_date = due_date
|
||||||
|
|
||||||
# save custom fields and ticket changes
|
# save custom fields and ticket changes
|
||||||
if customfields_form and customfields_form.is_valid():
|
if customfields_form and customfields_form.is_valid():
|
||||||
customfields_form.save(followup=f)
|
customfields_form.save(followup=f)
|
||||||
|
|
||||||
for checklist in ticket.checklists.all():
|
for checklist in ticket.checklists.all():
|
||||||
if checklist.id not in new_checklists:
|
if checklist.id not in new_checklists:
|
||||||
continue
|
continue
|
||||||
@ -327,24 +331,22 @@ def update_ticket(
|
|||||||
# Add completion if it was not done yet
|
# Add completion if it was not done yet
|
||||||
if not task.completion_date and task.id in new_completed_tasks:
|
if not task.completion_date and task.id in new_completed_tasks:
|
||||||
task.completion_date = timezone.now()
|
task.completion_date = timezone.now()
|
||||||
changed = 'completed'
|
changed = "completed"
|
||||||
# Remove it if it was done before
|
# Remove it if it was done before
|
||||||
elif task.completion_date and task.id not in new_completed_tasks:
|
elif task.completion_date and task.id not in new_completed_tasks:
|
||||||
task.completion_date = None
|
task.completion_date = None
|
||||||
changed = 'uncompleted'
|
changed = "uncompleted"
|
||||||
|
|
||||||
# Save and add ticket change if task state has changed
|
# Save and add ticket change if task state has changed
|
||||||
if changed:
|
if changed:
|
||||||
task.save(update_fields=['completion_date'])
|
task.save(update_fields=["completion_date"])
|
||||||
f.ticketchange_set.create(
|
f.ticketchange_set.create(
|
||||||
field=f'[{checklist.name}] {task.description}',
|
field=f"[{checklist.name}] {task.description}",
|
||||||
old_value=_('To do') if changed == 'completed' else _('Completed'),
|
old_value=_("To do") if changed == "completed" else _("Completed"),
|
||||||
new_value=_('Completed') if changed == 'completed' else _('To do'),
|
new_value=_("Completed") if changed == "completed" else _("To do"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if new_status in (
|
if new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS) and (
|
||||||
Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS
|
|
||||||
) and (
|
|
||||||
new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None
|
new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None
|
||||||
):
|
):
|
||||||
ticket.resolution = comment
|
ticket.resolution = comment
|
||||||
@ -363,32 +365,34 @@ def update_ticket(
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
ticket = update_messages_sent_to_by_public_and_status(
|
ticket = update_messages_sent_to_by_public_and_status(
|
||||||
public,
|
public, ticket, f, context, messages_sent_to, files
|
||||||
ticket,
|
|
||||||
f,
|
|
||||||
context,
|
|
||||||
messages_sent_to,
|
|
||||||
files
|
|
||||||
)
|
)
|
||||||
|
|
||||||
template_staff, template_cc = get_template_staff_and_template_cc(reassigned, f)
|
template_staff, template_cc = get_template_staff_and_template_cc(reassigned, f)
|
||||||
if ticket.assigned_to and (
|
if ticket.assigned_to and (
|
||||||
ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change
|
ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change
|
||||||
or (reassigned and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign)
|
or (
|
||||||
|
reassigned
|
||||||
|
and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign
|
||||||
|
)
|
||||||
):
|
):
|
||||||
messages_sent_to.update(ticket.send(
|
messages_sent_to.update(
|
||||||
{'assigned_to': (template_staff, context)},
|
ticket.send(
|
||||||
|
{"assigned_to": (template_staff, context)},
|
||||||
|
dont_send_to=messages_sent_to,
|
||||||
|
fail_silently=True,
|
||||||
|
files=files,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
messages_sent_to.update(
|
||||||
|
ticket.send(
|
||||||
|
{"ticket_cc": (template_cc, context)},
|
||||||
dont_send_to=messages_sent_to,
|
dont_send_to=messages_sent_to,
|
||||||
fail_silently=True,
|
fail_silently=True,
|
||||||
files=files,
|
files=files,
|
||||||
))
|
)
|
||||||
|
)
|
||||||
messages_sent_to.update(ticket.send(
|
|
||||||
{'ticket_cc': (template_cc, context)},
|
|
||||||
dont_send_to=messages_sent_to,
|
|
||||||
fail_silently=True,
|
|
||||||
files=files,
|
|
||||||
))
|
|
||||||
ticket.save()
|
ticket.save()
|
||||||
|
|
||||||
# emit signal with followup when the ticket update is done
|
# emit signal with followup when the ticket update is done
|
||||||
@ -398,4 +402,3 @@ def update_ticket(
|
|||||||
# auto subscribe user if enabled
|
# auto subscribe user if enabled
|
||||||
add_staff_subscription(user, ticket)
|
add_staff_subscription(user, ticket)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
@ -14,7 +14,13 @@ from django.views.generic import TemplateView
|
|||||||
from helpdesk import settings as helpdesk_settings
|
from helpdesk import settings as helpdesk_settings
|
||||||
from helpdesk.decorators import helpdesk_staff_member_required, protect_view
|
from helpdesk.decorators import helpdesk_staff_member_required, protect_view
|
||||||
from helpdesk.views import feeds, login, public, staff
|
from helpdesk.views import feeds, login, public, staff
|
||||||
from helpdesk.views.api import CreateUserView, FollowUpAttachmentViewSet, FollowUpViewSet, TicketViewSet, UserTicketViewSet
|
from helpdesk.views.api import (
|
||||||
|
CreateUserView,
|
||||||
|
FollowUpAttachmentViewSet,
|
||||||
|
FollowUpViewSet,
|
||||||
|
TicketViewSet,
|
||||||
|
UserTicketViewSet,
|
||||||
|
)
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
|
||||||
@ -63,16 +69,12 @@ urlpatterns = [
|
|||||||
name="followup_delete",
|
name="followup_delete",
|
||||||
),
|
),
|
||||||
path("tickets/<int:ticket_id>/edit/", staff.edit_ticket, name="edit"),
|
path("tickets/<int:ticket_id>/edit/", staff.edit_ticket, name="edit"),
|
||||||
path("tickets/<int:ticket_id>/update/",
|
path("tickets/<int:ticket_id>/update/", staff.update_ticket_view, name="update"),
|
||||||
staff.update_ticket_view, name="update"),
|
path("tickets/<int:ticket_id>/delete/", staff.delete_ticket, name="delete"),
|
||||||
path("tickets/<int:ticket_id>/delete/",
|
|
||||||
staff.delete_ticket, name="delete"),
|
|
||||||
path("tickets/<int:ticket_id>/hold/", staff.hold_ticket, name="hold"),
|
path("tickets/<int:ticket_id>/hold/", staff.hold_ticket, name="hold"),
|
||||||
path("tickets/<int:ticket_id>/unhold/",
|
path("tickets/<int:ticket_id>/unhold/", staff.unhold_ticket, name="unhold"),
|
||||||
staff.unhold_ticket, name="unhold"),
|
|
||||||
path("tickets/<int:ticket_id>/cc/", staff.ticket_cc, name="ticket_cc"),
|
path("tickets/<int:ticket_id>/cc/", staff.ticket_cc, name="ticket_cc"),
|
||||||
path("tickets/<int:ticket_id>/cc/add/",
|
path("tickets/<int:ticket_id>/cc/add/", staff.ticket_cc_add, name="ticket_cc_add"),
|
||||||
staff.ticket_cc_add, name="ticket_cc_add"),
|
|
||||||
path(
|
path(
|
||||||
"tickets/<int:ticket_id>/cc/delete/<int:cc_id>/",
|
"tickets/<int:ticket_id>/cc/delete/<int:cc_id>/",
|
||||||
staff.ticket_cc_del,
|
staff.ticket_cc_del,
|
||||||
@ -106,35 +108,33 @@ urlpatterns = [
|
|||||||
path(
|
path(
|
||||||
"tickets/<int:ticket_id>/checklists/<int:checklist_id>/",
|
"tickets/<int:ticket_id>/checklists/<int:checklist_id>/",
|
||||||
staff.edit_ticket_checklist,
|
staff.edit_ticket_checklist,
|
||||||
name="edit_ticket_checklist"
|
name="edit_ticket_checklist",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"tickets/<int:ticket_id>/checklists/<int:checklist_id>/delete/",
|
"tickets/<int:ticket_id>/checklists/<int:checklist_id>/delete/",
|
||||||
staff.delete_ticket_checklist,
|
staff.delete_ticket_checklist,
|
||||||
name="delete_ticket_checklist"
|
name="delete_ticket_checklist",
|
||||||
),
|
),
|
||||||
re_path(r"^raw/(?P<type_>\w+)/$", staff.raw_details, name="raw"),
|
re_path(r"^raw/(?P<type_>\w+)/$", staff.raw_details, name="raw"),
|
||||||
path("rss/", staff.rss_list, name="rss_index"),
|
path("rss/", staff.rss_list, name="rss_index"),
|
||||||
path("reports/", staff.report_index, name="report_index"),
|
path("reports/", staff.report_index, name="report_index"),
|
||||||
re_path(r"^reports/(?P<report>\w+)/$",
|
re_path(r"^reports/(?P<report>\w+)/$", staff.run_report, name="run_report"),
|
||||||
staff.run_report, name="run_report"),
|
|
||||||
path("save_query/", staff.save_query, name="savequery"),
|
path("save_query/", staff.save_query, name="savequery"),
|
||||||
path("delete_query/<int:pk>/", staff.delete_saved_query, name="delete_query"),
|
path("delete_query/<int:pk>/", staff.delete_saved_query, name="delete_query"),
|
||||||
path("settings/", staff.EditUserSettingsView.as_view(), name="user_settings"),
|
path("settings/", staff.EditUserSettingsView.as_view(), name="user_settings"),
|
||||||
path("ignore/", staff.email_ignore, name="email_ignore"),
|
path("ignore/", staff.email_ignore, name="email_ignore"),
|
||||||
path("ignore/add/", staff.email_ignore_add, name="email_ignore_add"),
|
path("ignore/add/", staff.email_ignore_add, name="email_ignore_add"),
|
||||||
path("ignore/delete/<int:id>/",
|
path("ignore/delete/<int:id>/", staff.email_ignore_del, name="email_ignore_del"),
|
||||||
staff.email_ignore_del, name="email_ignore_del"),
|
|
||||||
path("checklist-templates/", staff.checklist_templates, name="checklist_templates"),
|
path("checklist-templates/", staff.checklist_templates, name="checklist_templates"),
|
||||||
path(
|
path(
|
||||||
"checklist-templates/<int:checklist_template_id>/",
|
"checklist-templates/<int:checklist_template_id>/",
|
||||||
staff.checklist_templates,
|
staff.checklist_templates,
|
||||||
name="edit_checklist_template"
|
name="edit_checklist_template",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"checklist-templates/<int:checklist_template_id>/delete/",
|
"checklist-templates/<int:checklist_template_id>/delete/",
|
||||||
staff.delete_checklist_template,
|
staff.delete_checklist_template,
|
||||||
name="delete_checklist_template"
|
name="delete_checklist_template",
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^datatables_ticket_list/(?P<query>{})$".format(base64_pattern),
|
r"^datatables_ticket_list/(?P<query>{})$".format(base64_pattern),
|
||||||
@ -164,7 +164,11 @@ if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET:
|
|||||||
|
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
path("", protect_view(public.Homepage.as_view()), name="home"),
|
path("", protect_view(public.Homepage.as_view()), name="home"),
|
||||||
path("tickets/my-tickets/", protect_view(public.MyTickets.as_view()), name="my-tickets"),
|
path(
|
||||||
|
"tickets/my-tickets/",
|
||||||
|
protect_view(public.MyTickets.as_view()),
|
||||||
|
name="my-tickets",
|
||||||
|
),
|
||||||
path("tickets/submit/", public.create_ticket, name="submit"),
|
path("tickets/submit/", public.create_ticket, name="submit"),
|
||||||
path(
|
path(
|
||||||
"tickets/submit_iframe/",
|
"tickets/submit_iframe/",
|
||||||
@ -177,8 +181,7 @@ urlpatterns += [
|
|||||||
name="success_iframe",
|
name="success_iframe",
|
||||||
),
|
),
|
||||||
path("view/", protect_view(public.ViewTicket.as_view()), name="public_view"),
|
path("view/", protect_view(public.ViewTicket.as_view()), name="public_view"),
|
||||||
path("change_language/", public.change_language,
|
path("change_language/", public.change_language, name="public_change_language"),
|
||||||
name="public_change_language"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
@ -214,8 +217,9 @@ router = DefaultRouter()
|
|||||||
router.register(r"tickets", TicketViewSet, basename="ticket")
|
router.register(r"tickets", TicketViewSet, basename="ticket")
|
||||||
router.register(r"user_tickets", UserTicketViewSet, basename="user_tickets")
|
router.register(r"user_tickets", UserTicketViewSet, basename="user_tickets")
|
||||||
router.register(r"followups", FollowUpViewSet, basename="followups")
|
router.register(r"followups", FollowUpViewSet, basename="followups")
|
||||||
router.register(r"followups-attachments",
|
router.register(
|
||||||
FollowUpAttachmentViewSet, basename="followupattachments")
|
r"followups-attachments", FollowUpAttachmentViewSet, basename="followupattachments"
|
||||||
|
)
|
||||||
router.register(r"users", CreateUserView, basename="user")
|
router.register(r"users", CreateUserView, basename="user")
|
||||||
urlpatterns += [re_path(r"^api/", include(router.urls))]
|
urlpatterns += [re_path(r"^api/", include(router.urls))]
|
||||||
|
|
||||||
@ -249,8 +253,7 @@ urlpatterns += [
|
|||||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
path("kb/", kb.index, name="kb_index"),
|
path("kb/", kb.index, name="kb_index"),
|
||||||
re_path(r"^kb/(?P<slug>[A-Za-z0-9_-]+)/$",
|
re_path(r"^kb/(?P<slug>[A-Za-z0-9_-]+)/$", kb.category, name="kb_category"),
|
||||||
kb.category, name="kb_category"),
|
|
||||||
re_path(r"^kb/(?P<item>\d+)/vote/(?P<vote>up|down)/$", kb.vote, name="kb_vote"),
|
re_path(r"^kb/(?P<item>\d+)/vote/(?P<vote>up|down)/$", kb.vote, name="kb_vote"),
|
||||||
re_path(
|
re_path(
|
||||||
r"^kb_iframe/(?P<slug>[A-Za-z0-9_-]+)/$",
|
r"^kb_iframe/(?P<slug>[A-Za-z0-9_-]+)/$",
|
||||||
@ -268,8 +271,7 @@ urlpatterns += [
|
|||||||
path(
|
path(
|
||||||
"system_settings/",
|
"system_settings/",
|
||||||
login_required(
|
login_required(
|
||||||
DirectTemplateView.as_view(
|
DirectTemplateView.as_view(template_name="helpdesk/system_settings.html")
|
||||||
template_name="helpdesk/system_settings.html")
|
|
||||||
),
|
),
|
||||||
name="system_settings",
|
name="system_settings",
|
||||||
),
|
),
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
from helpdesk import settings as helpdesk_settings
|
from helpdesk import settings as helpdesk_settings
|
||||||
from helpdesk.models import Queue, Ticket
|
from helpdesk.models import Queue, Ticket
|
||||||
|
|
||||||
@ -23,14 +22,13 @@ class HelpdeskUser:
|
|||||||
"""
|
"""
|
||||||
user = self.user
|
user = self.user
|
||||||
all_queues = Queue.objects.all()
|
all_queues = Queue.objects.all()
|
||||||
public_ids = [q.pk for q in
|
public_ids = [q.pk for q in Queue.objects.filter(allow_public_submission=True)]
|
||||||
Queue.objects.filter(allow_public_submission=True)]
|
limit_queues_by_user = (
|
||||||
limit_queues_by_user = \
|
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION
|
||||||
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION \
|
|
||||||
and not user.is_superuser
|
and not user.is_superuser
|
||||||
|
)
|
||||||
if limit_queues_by_user:
|
if limit_queues_by_user:
|
||||||
id_list = [q.pk for q in all_queues if user.has_perm(
|
id_list = [q.pk for q in all_queues if user.has_perm(q.permission_name)]
|
||||||
q.permission_name)]
|
|
||||||
id_list += public_ids
|
id_list += public_ids
|
||||||
return all_queues.filter(pk__in=id_list)
|
return all_queues.filter(pk__in=id_list)
|
||||||
else:
|
else:
|
||||||
@ -56,8 +54,11 @@ class HelpdeskUser:
|
|||||||
return Ticket.objects.filter(queue__in=self.get_queues())
|
return Ticket.objects.filter(queue__in=self.get_queues())
|
||||||
|
|
||||||
def has_full_access(self):
|
def has_full_access(self):
|
||||||
return self.user.is_superuser or self.user.is_staff \
|
return (
|
||||||
|
self.user.is_superuser
|
||||||
|
or self.user.is_staff
|
||||||
or helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
or helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
||||||
|
)
|
||||||
|
|
||||||
def can_access_queue(self, queue):
|
def can_access_queue(self, queue):
|
||||||
"""Check if a certain user can access a certain queue.
|
"""Check if a certain user can access a certain queue.
|
||||||
@ -71,18 +72,18 @@ class HelpdeskUser:
|
|||||||
else:
|
else:
|
||||||
return (
|
return (
|
||||||
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION
|
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION
|
||||||
and
|
and self.user.has_perm(queue.permission_name)
|
||||||
self.user.has_perm(queue.permission_name)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def can_access_ticket(self, ticket):
|
def can_access_ticket(self, ticket):
|
||||||
"""Check to see if the user has permission to access
|
"""Check to see if the user has permission to access
|
||||||
a ticket. If not then deny access."""
|
a ticket. If not then deny access."""
|
||||||
user = self.user
|
user = self.user
|
||||||
if self.can_access_queue(ticket.queue):
|
if self.can_access_queue(ticket.queue):
|
||||||
return True
|
return True
|
||||||
elif self.has_full_access() or \
|
elif self.has_full_access() or (
|
||||||
(ticket.assigned_to and user.id == ticket.assigned_to.id):
|
ticket.assigned_to and user.id == ticket.assigned_to.id
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@ -90,4 +91,6 @@ class HelpdeskUser:
|
|||||||
def can_access_kbcategory(self, category):
|
def can_access_kbcategory(self, category):
|
||||||
if category.public:
|
if category.public:
|
||||||
return True
|
return True
|
||||||
return self.has_full_access() or (category.queue and self.can_access_queue(category.queue))
|
return self.has_full_access() or (
|
||||||
|
category.queue and self.can_access_queue(category.queue)
|
||||||
|
)
|
||||||
|
@ -14,6 +14,7 @@ from helpdesk import settings as helpdesk_settings
|
|||||||
def validate_file_extension(value):
|
def validate_file_extension(value):
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
import os
|
import os
|
||||||
|
|
||||||
ext = os.path.splitext(value.name)[1] # [0] returns path+filename
|
ext = os.path.splitext(value.name)[1] # [0] returns path+filename
|
||||||
# TODO: we might improve this with more thorough checks of file types
|
# TODO: we might improve this with more thorough checks of file types
|
||||||
# rather than just the extensions.
|
# rather than just the extensions.
|
||||||
@ -24,7 +25,5 @@ def validate_file_extension(value):
|
|||||||
if ext.lower() not in helpdesk_settings.HELPDESK_VALID_EXTENSIONS:
|
if ext.lower() not in helpdesk_settings.HELPDESK_VALID_EXTENSIONS:
|
||||||
# TODO: one more check in case it is a file with no extension; we
|
# TODO: one more check in case it is a file with no extension; we
|
||||||
# should always allow that?
|
# should always allow that?
|
||||||
if not (ext.lower() == '' or ext.lower() == '.'):
|
if not (ext.lower() == "" or ext.lower() == "."):
|
||||||
raise ValidationError(
|
raise ValidationError(_("Unsupported file extension: ") + ext.lower())
|
||||||
_('Unsupported file extension: ') + ext.lower()
|
|
||||||
)
|
|
||||||
|
@ -1,23 +1,28 @@
|
|||||||
from helpdesk.models import CustomField, KBItem, Queue
|
from helpdesk.models import CustomField, KBItem, Queue
|
||||||
|
|
||||||
|
|
||||||
class AbstractCreateTicketMixin():
|
class AbstractCreateTicketMixin:
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
initial_data = {}
|
initial_data = {}
|
||||||
request = self.request
|
request = self.request
|
||||||
try:
|
try:
|
||||||
initial_data['queue'] = Queue.objects.get(
|
initial_data["queue"] = Queue.objects.get(
|
||||||
slug=request.GET.get('queue', None)).id
|
slug=request.GET.get("queue", None)
|
||||||
|
).id
|
||||||
except Queue.DoesNotExist:
|
except Queue.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
u = request.user
|
u = request.user
|
||||||
if u.is_authenticated and u.usersettings_helpdesk.use_email_as_submitter and u.email:
|
if (
|
||||||
initial_data['submitter_email'] = u.email
|
u.is_authenticated
|
||||||
|
and u.usersettings_helpdesk.use_email_as_submitter
|
||||||
|
and u.email
|
||||||
|
):
|
||||||
|
initial_data["submitter_email"] = u.email
|
||||||
|
|
||||||
query_param_fields = ['submitter_email',
|
query_param_fields = ["submitter_email", "title", "body", "queue", "kbitem"]
|
||||||
'title', 'body', 'queue', 'kbitem']
|
|
||||||
custom_fields = [
|
custom_fields = [
|
||||||
"custom_%s" % f.name for f in CustomField.objects.filter(staff_only=False)]
|
"custom_%s" % f.name for f in CustomField.objects.filter(staff_only=False)
|
||||||
|
]
|
||||||
query_param_fields += custom_fields
|
query_param_fields += custom_fields
|
||||||
for qpf in query_param_fields:
|
for qpf in query_param_fields:
|
||||||
initial_data[qpf] = request.GET.get(qpf, initial_data.get(qpf, ""))
|
initial_data[qpf] = request.GET.get(qpf, initial_data.get(qpf, ""))
|
||||||
@ -27,13 +32,12 @@ class AbstractCreateTicketMixin():
|
|||||||
def get_form_kwargs(self, *args, **kwargs):
|
def get_form_kwargs(self, *args, **kwargs):
|
||||||
kwargs = super().get_form_kwargs(*args, **kwargs)
|
kwargs = super().get_form_kwargs(*args, **kwargs)
|
||||||
kbitem = self.request.GET.get(
|
kbitem = self.request.GET.get(
|
||||||
'kbitem',
|
"kbitem",
|
||||||
self.request.POST.get('kbitem', None),
|
self.request.POST.get("kbitem", None),
|
||||||
)
|
)
|
||||||
if kbitem:
|
if kbitem:
|
||||||
try:
|
try:
|
||||||
kwargs['kbcategory'] = KBItem.objects.get(
|
kwargs["kbcategory"] = KBItem.objects.get(pk=int(kbitem)).category
|
||||||
pk=int(kbitem)).category
|
|
||||||
except (ValueError, KBItem.DoesNotExist):
|
except (ValueError, KBItem.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
return kwargs
|
return kwargs
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from helpdesk.models import FollowUp, FollowUpAttachment, Ticket
|
from helpdesk.models import FollowUp, FollowUpAttachment, Ticket
|
||||||
from helpdesk.serializers import FollowUpAttachmentSerializer, FollowUpSerializer, TicketSerializer, UserSerializer, PublicTicketListingSerializer
|
from helpdesk.serializers import (
|
||||||
|
FollowUpAttachmentSerializer,
|
||||||
|
FollowUpSerializer,
|
||||||
|
TicketSerializer,
|
||||||
|
UserSerializer,
|
||||||
|
PublicTicketListingSerializer,
|
||||||
|
)
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.mixins import CreateModelMixin
|
from rest_framework.mixins import CreateModelMixin
|
||||||
from rest_framework.permissions import IsAdminUser, IsAuthenticated
|
from rest_framework.permissions import IsAdminUser, IsAuthenticated
|
||||||
@ -12,7 +18,7 @@ from helpdesk import settings as helpdesk_settings
|
|||||||
|
|
||||||
class ConservativePagination(PageNumberPagination):
|
class ConservativePagination(PageNumberPagination):
|
||||||
page_size = 25
|
page_size = 25
|
||||||
page_size_query_param = 'page_size'
|
page_size_query_param = "page_size"
|
||||||
|
|
||||||
|
|
||||||
class UserTicketViewSet(viewsets.ReadOnlyModelViewSet):
|
class UserTicketViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
@ -21,18 +27,20 @@ class UserTicketViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
|
|
||||||
The view is paginated by default
|
The view is paginated by default
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = PublicTicketListingSerializer
|
serializer_class = PublicTicketListingSerializer
|
||||||
pagination_class = ConservativePagination
|
pagination_class = ConservativePagination
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
tickets = Ticket.objects.filter(submitter_email=self.request.user.email).order_by('-created')
|
tickets = Ticket.objects.filter(
|
||||||
|
submitter_email=self.request.user.email
|
||||||
|
).order_by("-created")
|
||||||
for ticket in tickets:
|
for ticket in tickets:
|
||||||
ticket.set_custom_field_values()
|
ticket.set_custom_field_values()
|
||||||
return tickets
|
return tickets
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TicketViewSet(viewsets.ModelViewSet):
|
class TicketViewSet(viewsets.ModelViewSet):
|
||||||
"""
|
"""
|
||||||
A viewset that provides the standard actions to handle Ticket
|
A viewset that provides the standard actions to handle Ticket
|
||||||
@ -41,6 +49,7 @@ class TicketViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
`/api/tickets/?status=Open,Resolved` will return all the tickets that are Open or Resolved.
|
`/api/tickets/?status=Open,Resolved` will return all the tickets that are Open or Resolved.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = Ticket.objects.all()
|
queryset = Ticket.objects.all()
|
||||||
serializer_class = TicketSerializer
|
serializer_class = TicketSerializer
|
||||||
pagination_class = ConservativePagination
|
pagination_class = ConservativePagination
|
||||||
@ -50,17 +59,17 @@ class TicketViewSet(viewsets.ModelViewSet):
|
|||||||
tickets = Ticket.objects.all()
|
tickets = Ticket.objects.all()
|
||||||
|
|
||||||
# filter by status
|
# filter by status
|
||||||
status = self.request.query_params.get('status', None)
|
status = self.request.query_params.get("status", None)
|
||||||
if status:
|
if status:
|
||||||
statuses = status.split(',') if status else []
|
statuses = status.split(",") if status else []
|
||||||
status_choices = helpdesk_settings.TICKET_STATUS_CHOICES
|
status_choices = helpdesk_settings.TICKET_STATUS_CHOICES
|
||||||
number_statuses = []
|
number_statuses = []
|
||||||
for status in statuses:
|
for status in statuses:
|
||||||
for choice in status_choices:
|
for choice in status_choices:
|
||||||
if str(choice[0]) == status:
|
if str(choice[0]) == status:
|
||||||
number_statuses.append(choice[0])
|
number_statuses.append(choice[0])
|
||||||
if number_statuses:
|
if number_statuses:
|
||||||
tickets = tickets.filter(status__in=number_statuses)
|
tickets = tickets.filter(status__in=number_statuses)
|
||||||
|
|
||||||
for ticket in tickets:
|
for ticket in tickets:
|
||||||
ticket.set_custom_field_values()
|
ticket.set_custom_field_values()
|
||||||
|
@ -25,8 +25,8 @@ for open_status in Ticket.OPEN_STATUSES:
|
|||||||
|
|
||||||
|
|
||||||
class OpenTicketsByUser(Feed):
|
class OpenTicketsByUser(Feed):
|
||||||
title_template = 'helpdesk/rss/ticket_title.html'
|
title_template = "helpdesk/rss/ticket_title.html"
|
||||||
description_template = 'helpdesk/rss/ticket_description.html'
|
description_template = "helpdesk/rss/ticket_description.html"
|
||||||
|
|
||||||
def get_object(self, request, user_name, queue_slug=None):
|
def get_object(self, request, user_name, queue_slug=None):
|
||||||
user = get_object_or_404(User, username=user_name)
|
user = get_object_or_404(User, username=user_name)
|
||||||
@ -35,54 +35,56 @@ class OpenTicketsByUser(Feed):
|
|||||||
else:
|
else:
|
||||||
queue = None
|
queue = None
|
||||||
|
|
||||||
return {'user': user, 'queue': queue}
|
return {"user": user, "queue": queue}
|
||||||
|
|
||||||
def title(self, obj):
|
def title(self, obj):
|
||||||
if obj['queue']:
|
if obj["queue"]:
|
||||||
return _("Helpdesk: Open Tickets in queue %(queue)s for %(username)s") % {
|
return _("Helpdesk: Open Tickets in queue %(queue)s for %(username)s") % {
|
||||||
'queue': obj['queue'].title,
|
"queue": obj["queue"].title,
|
||||||
'username': obj['user'].get_username(),
|
"username": obj["user"].get_username(),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return _("Helpdesk: Open Tickets for %(username)s") % {
|
return _("Helpdesk: Open Tickets for %(username)s") % {
|
||||||
'username': obj['user'].get_username(),
|
"username": obj["user"].get_username(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def description(self, obj):
|
def description(self, obj):
|
||||||
if obj['queue']:
|
if obj["queue"]:
|
||||||
return _("Open and Reopened Tickets in queue %(queue)s for %(username)s") % {
|
return _(
|
||||||
'queue': obj['queue'].title,
|
"Open and Reopened Tickets in queue %(queue)s for %(username)s"
|
||||||
'username': obj['user'].get_username(),
|
) % {
|
||||||
|
"queue": obj["queue"].title,
|
||||||
|
"username": obj["user"].get_username(),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return _("Open and Reopened Tickets for %(username)s") % {
|
return _("Open and Reopened Tickets for %(username)s") % {
|
||||||
'username': obj['user'].get_username(),
|
"username": obj["user"].get_username(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def link(self, obj):
|
def link(self, obj):
|
||||||
if obj['queue']:
|
if obj["queue"]:
|
||||||
return u'%s?assigned_to=%s&queue=%s' % (
|
return "%s?assigned_to=%s&queue=%s" % (
|
||||||
reverse('helpdesk:list'),
|
reverse("helpdesk:list"),
|
||||||
obj['user'].id,
|
obj["user"].id,
|
||||||
obj['queue'].id,
|
obj["queue"].id,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return u'%s?assigned_to=%s' % (
|
return "%s?assigned_to=%s" % (
|
||||||
reverse('helpdesk:list'),
|
reverse("helpdesk:list"),
|
||||||
obj['user'].id,
|
obj["user"].id,
|
||||||
)
|
)
|
||||||
|
|
||||||
def items(self, obj):
|
def items(self, obj):
|
||||||
if obj['queue']:
|
if obj["queue"]:
|
||||||
return Ticket.objects.filter(
|
return (
|
||||||
assigned_to=obj['user']
|
Ticket.objects.filter(assigned_to=obj["user"])
|
||||||
).filter(
|
.filter(queue=obj["queue"])
|
||||||
queue=obj['queue']
|
.filter(Q_OPEN_STATUSES)
|
||||||
).filter(Q_OPEN_STATUSES)
|
)
|
||||||
else:
|
else:
|
||||||
return Ticket.objects.filter(
|
return Ticket.objects.filter(assigned_to=obj["user"]).filter(
|
||||||
assigned_to=obj['user']
|
Q_OPEN_STATUSES
|
||||||
).filter(Q_OPEN_STATUSES)
|
)
|
||||||
|
|
||||||
def item_pubdate(self, item):
|
def item_pubdate(self, item):
|
||||||
return item.created
|
return item.created
|
||||||
@ -91,21 +93,19 @@ class OpenTicketsByUser(Feed):
|
|||||||
if item.assigned_to:
|
if item.assigned_to:
|
||||||
return item.assigned_to.get_username()
|
return item.assigned_to.get_username()
|
||||||
else:
|
else:
|
||||||
return _('Unassigned')
|
return _("Unassigned")
|
||||||
|
|
||||||
|
|
||||||
class UnassignedTickets(Feed):
|
class UnassignedTickets(Feed):
|
||||||
title_template = 'helpdesk/rss/ticket_title.html'
|
title_template = "helpdesk/rss/ticket_title.html"
|
||||||
description_template = 'helpdesk/rss/ticket_description.html'
|
description_template = "helpdesk/rss/ticket_description.html"
|
||||||
|
|
||||||
title = _('Helpdesk: Unassigned Tickets')
|
title = _("Helpdesk: Unassigned Tickets")
|
||||||
description = _('Unassigned Open and Reopened tickets')
|
description = _("Unassigned Open and Reopened tickets")
|
||||||
link = '' # '%s?assigned_to=' % reverse('helpdesk:list')
|
link = "" # '%s?assigned_to=' % reverse('helpdesk:list')
|
||||||
|
|
||||||
def items(self, obj):
|
def items(self, obj):
|
||||||
return Ticket.objects.filter(
|
return Ticket.objects.filter(assigned_to__isnull=True).filter(Q_OPEN_STATUSES)
|
||||||
assigned_to__isnull=True
|
|
||||||
).filter(Q_OPEN_STATUSES)
|
|
||||||
|
|
||||||
def item_pubdate(self, item):
|
def item_pubdate(self, item):
|
||||||
return item.created
|
return item.created
|
||||||
@ -114,49 +114,48 @@ class UnassignedTickets(Feed):
|
|||||||
if item.assigned_to:
|
if item.assigned_to:
|
||||||
return item.assigned_to.get_username()
|
return item.assigned_to.get_username()
|
||||||
else:
|
else:
|
||||||
return _('Unassigned')
|
return _("Unassigned")
|
||||||
|
|
||||||
|
|
||||||
class RecentFollowUps(Feed):
|
class RecentFollowUps(Feed):
|
||||||
title_template = 'helpdesk/rss/recent_activity_title.html'
|
title_template = "helpdesk/rss/recent_activity_title.html"
|
||||||
description_template = 'helpdesk/rss/recent_activity_description.html'
|
description_template = "helpdesk/rss/recent_activity_description.html"
|
||||||
|
|
||||||
title = _('Helpdesk: Recent Followups')
|
title = _("Helpdesk: Recent Followups")
|
||||||
description = _(
|
description = _(
|
||||||
'Recent FollowUps, such as e-mail replies, comments, attachments and resolutions')
|
"Recent FollowUps, such as e-mail replies, comments, attachments and resolutions"
|
||||||
link = '/tickets/' # reverse('helpdesk:list')
|
)
|
||||||
|
link = "/tickets/" # reverse('helpdesk:list')
|
||||||
|
|
||||||
def items(self):
|
def items(self):
|
||||||
return FollowUp.objects.order_by('-date')[:20]
|
return FollowUp.objects.order_by("-date")[:20]
|
||||||
|
|
||||||
|
|
||||||
class OpenTicketsByQueue(Feed):
|
class OpenTicketsByQueue(Feed):
|
||||||
title_template = 'helpdesk/rss/ticket_title.html'
|
title_template = "helpdesk/rss/ticket_title.html"
|
||||||
description_template = 'helpdesk/rss/ticket_description.html'
|
description_template = "helpdesk/rss/ticket_description.html"
|
||||||
|
|
||||||
def get_object(self, request, queue_slug):
|
def get_object(self, request, queue_slug):
|
||||||
return get_object_or_404(Queue, slug=queue_slug)
|
return get_object_or_404(Queue, slug=queue_slug)
|
||||||
|
|
||||||
def title(self, obj):
|
def title(self, obj):
|
||||||
return _('Helpdesk: Open Tickets in queue %(queue)s') % {
|
return _("Helpdesk: Open Tickets in queue %(queue)s") % {
|
||||||
'queue': obj.title,
|
"queue": obj.title,
|
||||||
}
|
}
|
||||||
|
|
||||||
def description(self, obj):
|
def description(self, obj):
|
||||||
return _('Open and Reopened Tickets in queue %(queue)s') % {
|
return _("Open and Reopened Tickets in queue %(queue)s") % {
|
||||||
'queue': obj.title,
|
"queue": obj.title,
|
||||||
}
|
}
|
||||||
|
|
||||||
def link(self, obj):
|
def link(self, obj):
|
||||||
return '%s?queue=%s' % (
|
return "%s?queue=%s" % (
|
||||||
reverse('helpdesk:list'),
|
reverse("helpdesk:list"),
|
||||||
obj.id,
|
obj.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
def items(self, obj):
|
def items(self, obj):
|
||||||
return Ticket.objects.filter(
|
return Ticket.objects.filter(queue=obj).filter(Q_OPEN_STATUSES)
|
||||||
queue=obj
|
|
||||||
).filter(Q_OPEN_STATUSES)
|
|
||||||
|
|
||||||
def item_pubdate(self, item):
|
def item_pubdate(self, item):
|
||||||
return item.created
|
return item.created
|
||||||
@ -165,4 +164,4 @@ class OpenTicketsByQueue(Feed):
|
|||||||
if item.assigned_to:
|
if item.assigned_to:
|
||||||
return item.assigned_to.get_username()
|
return item.assigned_to.get_username()
|
||||||
else:
|
else:
|
||||||
return _('Unassigned')
|
return _("Unassigned")
|
||||||
|
@ -18,10 +18,14 @@ from helpdesk.models import KBCategory, KBItem
|
|||||||
def index(request):
|
def index(request):
|
||||||
huser = user.huser_from_request(request)
|
huser = user.huser_from_request(request)
|
||||||
# TODO: It'd be great to have a list of most popular items here.
|
# TODO: It'd be great to have a list of most popular items here.
|
||||||
return render(request, 'helpdesk/kb_index.html', {
|
return render(
|
||||||
'kb_categories': huser.get_allowed_kb_categories(),
|
request,
|
||||||
'helpdesk_settings': helpdesk_settings,
|
"helpdesk/kb_index.html",
|
||||||
})
|
{
|
||||||
|
"kb_categories": huser.get_allowed_kb_categories(),
|
||||||
|
"helpdesk_settings": helpdesk_settings,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def category(request, slug, iframe=False):
|
def category(request, slug, iframe=False):
|
||||||
@ -29,29 +33,33 @@ def category(request, slug, iframe=False):
|
|||||||
if not user.huser_from_request(request).can_access_kbcategory(category):
|
if not user.huser_from_request(request).can_access_kbcategory(category):
|
||||||
raise Http404
|
raise Http404
|
||||||
items = category.kbitem_set.filter(enabled=True)
|
items = category.kbitem_set.filter(enabled=True)
|
||||||
selected_item = request.GET.get('kbitem', None)
|
selected_item = request.GET.get("kbitem", None)
|
||||||
try:
|
try:
|
||||||
selected_item = int(selected_item)
|
selected_item = int(selected_item)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
qparams = request.GET.copy()
|
qparams = request.GET.copy()
|
||||||
try:
|
try:
|
||||||
del qparams['kbitem']
|
del qparams["kbitem"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
template = 'helpdesk/kb_category.html'
|
template = "helpdesk/kb_category.html"
|
||||||
if iframe:
|
if iframe:
|
||||||
template = 'helpdesk/kb_category_iframe.html'
|
template = "helpdesk/kb_category_iframe.html"
|
||||||
staff = request.user.is_authenticated and request.user.is_staff
|
staff = request.user.is_authenticated and request.user.is_staff
|
||||||
return render(request, template, {
|
return render(
|
||||||
'category': category,
|
request,
|
||||||
'items': items,
|
template,
|
||||||
'selected_item': selected_item,
|
{
|
||||||
'query_param_string': qparams.urlencode(),
|
"category": category,
|
||||||
'helpdesk_settings': helpdesk_settings,
|
"items": items,
|
||||||
'iframe': iframe,
|
"selected_item": selected_item,
|
||||||
'staff': staff,
|
"query_param_string": qparams.urlencode(),
|
||||||
})
|
"helpdesk_settings": helpdesk_settings,
|
||||||
|
"iframe": iframe,
|
||||||
|
"staff": staff,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@xframe_options_exempt
|
@xframe_options_exempt
|
||||||
@ -62,7 +70,7 @@ def category_iframe(request, slug):
|
|||||||
def vote(request, item, vote):
|
def vote(request, item, vote):
|
||||||
item = get_object_or_404(KBItem, pk=item)
|
item = get_object_or_404(KBItem, pk=item)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if vote == 'up':
|
if vote == "up":
|
||||||
if not item.voted_by.filter(pk=request.user.pk):
|
if not item.voted_by.filter(pk=request.user.pk):
|
||||||
item.votes += 1
|
item.votes += 1
|
||||||
item.voted_by.add(request.user.pk)
|
item.voted_by.add(request.user.pk)
|
||||||
@ -70,7 +78,7 @@ def vote(request, item, vote):
|
|||||||
if item.downvoted_by.filter(pk=request.user.pk):
|
if item.downvoted_by.filter(pk=request.user.pk):
|
||||||
item.votes -= 1
|
item.votes -= 1
|
||||||
item.downvoted_by.remove(request.user.pk)
|
item.downvoted_by.remove(request.user.pk)
|
||||||
if vote == 'down':
|
if vote == "down":
|
||||||
if not item.downvoted_by.filter(pk=request.user.pk):
|
if not item.downvoted_by.filter(pk=request.user.pk):
|
||||||
item.votes += 1
|
item.votes += 1
|
||||||
item.downvoted_by.add(request.user.pk)
|
item.downvoted_by.add(request.user.pk)
|
||||||
|
@ -5,24 +5,22 @@ from django.shortcuts import resolve_url
|
|||||||
|
|
||||||
|
|
||||||
default_login_view = auth_views.LoginView.as_view(
|
default_login_view = auth_views.LoginView.as_view(
|
||||||
template_name='helpdesk/registration/login.html')
|
template_name="helpdesk/registration/login.html"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def login(request):
|
def login(request):
|
||||||
login_url = settings.LOGIN_URL
|
login_url = settings.LOGIN_URL
|
||||||
# Prevent redirect loop by checking that LOGIN_URL is not this view's name
|
# Prevent redirect loop by checking that LOGIN_URL is not this view's name
|
||||||
condition = (
|
condition = login_url and (
|
||||||
login_url
|
login_url != resolve_url(request.resolver_match.view_name)
|
||||||
and (
|
and (login_url != request.resolver_match.view_name)
|
||||||
login_url != resolve_url(request.resolver_match.view_name)
|
|
||||||
and (login_url != request.resolver_match.view_name)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if condition:
|
if condition:
|
||||||
if 'next' in request.GET:
|
if "next" in request.GET:
|
||||||
return_to = request.GET['next']
|
return_to = request.GET["next"]
|
||||||
else:
|
else:
|
||||||
return_to = resolve_url('helpdesk:home')
|
return_to = resolve_url("helpdesk:home")
|
||||||
return redirect_to_login(return_to, login_url)
|
return redirect_to_login(return_to, login_url)
|
||||||
else:
|
else:
|
||||||
return default_login_view(request)
|
return default_login_view(request)
|
||||||
|
@ -7,9 +7,12 @@ views/public.py - All public facing views, eg non-staff (no authentication
|
|||||||
required) views.
|
required) views.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied
|
from django.core.exceptions import (
|
||||||
|
ImproperlyConfigured,
|
||||||
|
ObjectDoesNotExist,
|
||||||
|
PermissionDenied,
|
||||||
|
)
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -41,11 +44,11 @@ def create_ticket(request, *args, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
|
class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
|
||||||
|
|
||||||
def get_form_class(self):
|
def get_form_class(self):
|
||||||
try:
|
try:
|
||||||
the_module, the_form_class = helpdesk_settings.HELPDESK_PUBLIC_TICKET_FORM_CLASS.rsplit(
|
the_module, the_form_class = (
|
||||||
".", 1)
|
helpdesk_settings.HELPDESK_PUBLIC_TICKET_FORM_CLASS.rsplit(".", 1)
|
||||||
|
)
|
||||||
the_module = import_module(the_module)
|
the_module = import_module(the_module)
|
||||||
the_form_class = getattr(the_module, the_form_class)
|
the_form_class = getattr(the_module, the_form_class)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -56,76 +59,85 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
|
|||||||
|
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
request = self.request
|
request = self.request
|
||||||
if not request.user.is_authenticated and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT:
|
if (
|
||||||
return HttpResponseRedirect(reverse('login'))
|
not request.user.is_authenticated
|
||||||
|
and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT
|
||||||
|
):
|
||||||
|
return HttpResponseRedirect(reverse("login"))
|
||||||
|
|
||||||
if is_helpdesk_staff(request.user) or \
|
if is_helpdesk_staff(request.user) or (
|
||||||
(request.user.is_authenticated and
|
request.user.is_authenticated
|
||||||
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE):
|
and helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
if request.user.usersettings_helpdesk.login_view_ticketlist:
|
if request.user.usersettings_helpdesk.login_view_ticketlist:
|
||||||
return HttpResponseRedirect(reverse('helpdesk:list'))
|
return HttpResponseRedirect(reverse("helpdesk:list"))
|
||||||
else:
|
else:
|
||||||
return HttpResponseRedirect(reverse('helpdesk:dashboard'))
|
return HttpResponseRedirect(reverse("helpdesk:dashboard"))
|
||||||
except UserSettings.DoesNotExist:
|
except UserSettings.DoesNotExist:
|
||||||
return HttpResponseRedirect(reverse('helpdesk:dashboard'))
|
return HttpResponseRedirect(reverse("helpdesk:dashboard"))
|
||||||
return super().dispatch(*args, **kwargs)
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
initial_data = super().get_initial()
|
initial_data = super().get_initial()
|
||||||
|
|
||||||
# add pre-defined data for public ticket
|
# add pre-defined data for public ticket
|
||||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE'):
|
if hasattr(settings, "HELPDESK_PUBLIC_TICKET_QUEUE"):
|
||||||
# get the requested queue; return an error if queue not found
|
# get the requested queue; return an error if queue not found
|
||||||
try:
|
try:
|
||||||
initial_data['queue'] = Queue.objects.get(
|
initial_data["queue"] = Queue.objects.get(
|
||||||
slug=settings.HELPDESK_PUBLIC_TICKET_QUEUE,
|
slug=settings.HELPDESK_PUBLIC_TICKET_QUEUE,
|
||||||
allow_public_submission=True
|
allow_public_submission=True,
|
||||||
).id
|
).id
|
||||||
except Queue.DoesNotExist as e:
|
except Queue.DoesNotExist as e:
|
||||||
logger.fatal(
|
logger.fatal(
|
||||||
"Public queue '%s' is configured as default but can't be found",
|
"Public queue '%s' is configured as default but can't be found",
|
||||||
settings.HELPDESK_PUBLIC_TICKET_QUEUE
|
settings.HELPDESK_PUBLIC_TICKET_QUEUE,
|
||||||
)
|
)
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured("Wrong public queue configuration") from e
|
||||||
"Wrong public queue configuration") from e
|
if hasattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY"):
|
||||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'):
|
initial_data["priority"] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY
|
||||||
initial_data['priority'] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY
|
if hasattr(settings, "HELPDESK_PUBLIC_TICKET_DUE_DATE"):
|
||||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_DUE_DATE'):
|
initial_data["due_date"] = settings.HELPDESK_PUBLIC_TICKET_DUE_DATE
|
||||||
initial_data['due_date'] = settings.HELPDESK_PUBLIC_TICKET_DUE_DATE
|
|
||||||
return initial_data
|
return initial_data
|
||||||
|
|
||||||
def get_form_kwargs(self, *args, **kwargs):
|
def get_form_kwargs(self, *args, **kwargs):
|
||||||
kwargs = super().get_form_kwargs(*args, **kwargs)
|
kwargs = super().get_form_kwargs(*args, **kwargs)
|
||||||
if '_hide_fields_' in self.request.GET:
|
if "_hide_fields_" in self.request.GET:
|
||||||
kwargs['hidden_fields'] = self.request.GET.get(
|
kwargs["hidden_fields"] = self.request.GET.get("_hide_fields_", "").split(
|
||||||
'_hide_fields_', '').split(',')
|
","
|
||||||
kwargs['readonly_fields'] = self.request.GET.get(
|
)
|
||||||
'_readonly_fields_', '').split(',')
|
kwargs["readonly_fields"] = self.request.GET.get("_readonly_fields_", "").split(
|
||||||
|
","
|
||||||
|
)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
request = self.request
|
request = self.request
|
||||||
if text_is_spam(form.cleaned_data['body'], request):
|
if text_is_spam(form.cleaned_data["body"], request):
|
||||||
# This submission is spam. Let's not save it.
|
# This submission is spam. Let's not save it.
|
||||||
return render(request, template_name='helpdesk/public_spam.html')
|
return render(request, template_name="helpdesk/public_spam.html")
|
||||||
else:
|
else:
|
||||||
ticket = form.save(
|
ticket = form.save(
|
||||||
user=self.request.user if self.request.user.is_authenticated else None)
|
user=self.request.user if self.request.user.is_authenticated else None
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
return HttpResponseRedirect('%s?ticket=%s&email=%s&key=%s' % (
|
return HttpResponseRedirect(
|
||||||
reverse('helpdesk:public_view'),
|
"%s?ticket=%s&email=%s&key=%s"
|
||||||
ticket.ticket_for_url,
|
% (
|
||||||
quote(ticket.submitter_email),
|
reverse("helpdesk:public_view"),
|
||||||
ticket.secret_key)
|
ticket.ticket_for_url,
|
||||||
|
quote(ticket.submitter_email),
|
||||||
|
ticket.secret_key,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# if someone enters a non-int string for the ticket
|
# if someone enters a non-int string for the ticket
|
||||||
return HttpResponseRedirect(reverse('helpdesk:home'))
|
return HttpResponseRedirect(reverse("helpdesk:home"))
|
||||||
|
|
||||||
|
|
||||||
class CreateTicketIframeView(BaseCreateTicketView):
|
class CreateTicketIframeView(BaseCreateTicketView):
|
||||||
template_name = 'helpdesk/public_create_ticket_iframe.html'
|
template_name = "helpdesk/public_create_ticket_iframe.html"
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@xframe_options_exempt
|
@xframe_options_exempt
|
||||||
@ -134,11 +146,11 @@ class CreateTicketIframeView(BaseCreateTicketView):
|
|||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
if super().form_valid(form).status_code == 302:
|
if super().form_valid(form).status_code == 302:
|
||||||
return HttpResponseRedirect(reverse('helpdesk:success_iframe'))
|
return HttpResponseRedirect(reverse("helpdesk:success_iframe"))
|
||||||
|
|
||||||
|
|
||||||
class SuccessIframeView(TemplateView):
|
class SuccessIframeView(TemplateView):
|
||||||
template_name = 'helpdesk/success_iframe.html'
|
template_name = "helpdesk/success_iframe.html"
|
||||||
|
|
||||||
@xframe_options_exempt
|
@xframe_options_exempt
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
@ -146,123 +158,140 @@ class SuccessIframeView(TemplateView):
|
|||||||
|
|
||||||
|
|
||||||
class CreateTicketView(BaseCreateTicketView):
|
class CreateTicketView(BaseCreateTicketView):
|
||||||
template_name = 'helpdesk/public_create_ticket.html'
|
template_name = "helpdesk/public_create_ticket.html"
|
||||||
|
|
||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
form = super().get_form(form_class)
|
form = super().get_form(form_class)
|
||||||
# Add the CSS error class to the form in order to better see them in
|
# Add the CSS error class to the form in order to better see them in
|
||||||
# the page
|
# the page
|
||||||
form.error_css_class = 'text-danger'
|
form.error_css_class = "text-danger"
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
|
||||||
class Homepage(CreateTicketView):
|
class Homepage(CreateTicketView):
|
||||||
template_name = 'helpdesk/public_homepage.html'
|
template_name = "helpdesk/public_homepage.html"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['kb_categories'] = huser_from_request(
|
context["kb_categories"] = huser_from_request(
|
||||||
self.request).get_allowed_kb_categories()
|
self.request
|
||||||
|
).get_allowed_kb_categories()
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class SearchForTicketView(TemplateView):
|
class SearchForTicketView(TemplateView):
|
||||||
template_name = 'helpdesk/public_view_form.html'
|
template_name = "helpdesk/public_view_form.html"
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC:
|
if (
|
||||||
|
hasattr(settings, "HELPDESK_VIEW_A_TICKET_PUBLIC")
|
||||||
|
and settings.HELPDESK_VIEW_A_TICKET_PUBLIC
|
||||||
|
):
|
||||||
context = self.get_context_data(**kwargs)
|
context = self.get_context_data(**kwargs)
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
else:
|
else:
|
||||||
raise PermissionDenied("Public viewing of tickets without a secret key is forbidden.")
|
raise PermissionDenied(
|
||||||
|
"Public viewing of tickets without a secret key is forbidden."
|
||||||
|
)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
request = self.request
|
request = self.request
|
||||||
email = request.GET.get('email', None)
|
email = request.GET.get("email", None)
|
||||||
error_message = kwargs.get('error_message', None)
|
error_message = kwargs.get("error_message", None)
|
||||||
|
|
||||||
context.update({
|
context.update(
|
||||||
'ticket': False,
|
{
|
||||||
'email': email,
|
"ticket": False,
|
||||||
'error_message': error_message,
|
"email": email,
|
||||||
'helpdesk_settings': helpdesk_settings,
|
"error_message": error_message,
|
||||||
})
|
"helpdesk_settings": helpdesk_settings,
|
||||||
|
}
|
||||||
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ViewTicket(TemplateView):
|
class ViewTicket(TemplateView):
|
||||||
template_name = 'helpdesk/public_view_ticket.html'
|
template_name = "helpdesk/public_view_ticket.html"
|
||||||
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
ticket_req = request.GET.get('ticket', None)
|
ticket_req = request.GET.get("ticket", None)
|
||||||
email = request.GET.get('email', None)
|
email = request.GET.get("email", None)
|
||||||
key = request.GET.get('key', '')
|
key = request.GET.get("key", "")
|
||||||
|
|
||||||
if not (ticket_req and email):
|
if not (ticket_req and email):
|
||||||
if ticket_req is None and email is None:
|
if ticket_req is None and email is None:
|
||||||
return SearchForTicketView.as_view()(request)
|
return SearchForTicketView.as_view()(request)
|
||||||
else:
|
else:
|
||||||
return SearchForTicketView.as_view()(request, _('Missing ticket ID or e-mail address. Please try again.'))
|
return SearchForTicketView.as_view()(
|
||||||
|
request, _("Missing ticket ID or e-mail address. Please try again.")
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req)
|
queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req)
|
||||||
if request.user.is_authenticated and request.user.email == email:
|
if request.user.is_authenticated and request.user.email == email:
|
||||||
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email)
|
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email)
|
||||||
elif hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC:
|
elif (
|
||||||
|
hasattr(settings, "HELPDESK_VIEW_A_TICKET_PUBLIC")
|
||||||
|
and settings.HELPDESK_VIEW_A_TICKET_PUBLIC
|
||||||
|
):
|
||||||
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email)
|
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email)
|
||||||
else:
|
else:
|
||||||
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email, secret_key__iexact=key)
|
ticket = Ticket.objects.get(
|
||||||
|
id=ticket_id, submitter_email__iexact=email, secret_key__iexact=key
|
||||||
|
)
|
||||||
except (ObjectDoesNotExist, ValueError):
|
except (ObjectDoesNotExist, ValueError):
|
||||||
return SearchForTicketView.as_view()(request, _('Invalid ticket ID or e-mail address. Please try again.'))
|
return SearchForTicketView.as_view()(
|
||||||
|
request, _("Invalid ticket ID or e-mail address. Please try again.")
|
||||||
|
)
|
||||||
|
|
||||||
if 'close' in request.GET and ticket.status == Ticket.RESOLVED_STATUS:
|
if "close" in request.GET and ticket.status == Ticket.RESOLVED_STATUS:
|
||||||
from helpdesk.update_ticket import update_ticket
|
from helpdesk.update_ticket import update_ticket
|
||||||
|
|
||||||
update_ticket(
|
update_ticket(
|
||||||
request.user,
|
request.user,
|
||||||
ticket,
|
ticket,
|
||||||
public=True,
|
public=True,
|
||||||
comment=_('Submitter accepted resolution and closed ticket'),
|
comment=_("Submitter accepted resolution and closed ticket"),
|
||||||
new_status=Ticket.CLOSED_STATUS,
|
new_status=Ticket.CLOSED_STATUS,
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(ticket.ticket_url)
|
return HttpResponseRedirect(ticket.ticket_url)
|
||||||
|
|
||||||
# Prepare context for rendering
|
# Prepare context for rendering
|
||||||
context = {
|
context = {
|
||||||
'key': key,
|
"key": key,
|
||||||
'mail': email,
|
"mail": email,
|
||||||
'ticket': ticket,
|
"ticket": ticket,
|
||||||
'helpdesk_settings': helpdesk_settings,
|
"helpdesk_settings": helpdesk_settings,
|
||||||
'next': self.get_next_url(ticket_id)
|
"next": self.get_next_url(ticket_id),
|
||||||
}
|
}
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
def get_next_url(self, ticket_id):
|
def get_next_url(self, ticket_id):
|
||||||
redirect_url = ''
|
redirect_url = ""
|
||||||
if is_helpdesk_staff(self.request.user):
|
if is_helpdesk_staff(self.request.user):
|
||||||
redirect_url = reverse('helpdesk:view', args=[ticket_id])
|
redirect_url = reverse("helpdesk:view", args=[ticket_id])
|
||||||
if 'close' in self.request.GET:
|
if "close" in self.request.GET:
|
||||||
redirect_url += '?close'
|
redirect_url += "?close"
|
||||||
elif helpdesk_settings.HELPDESK_NAVIGATION_ENABLED:
|
elif helpdesk_settings.HELPDESK_NAVIGATION_ENABLED:
|
||||||
redirect_url = reverse('helpdesk:view', args=[ticket_id])
|
redirect_url = reverse("helpdesk:view", args=[ticket_id])
|
||||||
return redirect_url
|
return redirect_url
|
||||||
|
|
||||||
|
|
||||||
class MyTickets(TemplateView):
|
class MyTickets(TemplateView):
|
||||||
template_name = 'helpdesk/my_tickets.html'
|
template_name = "helpdesk/my_tickets.html"
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return HttpResponseRedirect(reverse('helpdesk:login'))
|
return HttpResponseRedirect(reverse("helpdesk:login"))
|
||||||
|
|
||||||
context = self.get_context_data(**kwargs)
|
context = self.get_context_data(**kwargs)
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
|
|
||||||
def change_language(request):
|
def change_language(request):
|
||||||
return_to = ''
|
return_to = ""
|
||||||
if 'return_to' in request.GET:
|
if "return_to" in request.GET:
|
||||||
return_to = request.GET['return_to']
|
return_to = request.GET["return_to"]
|
||||||
|
|
||||||
return render(request, 'helpdesk/public_change_language.html', {'next': return_to})
|
return render(request, "helpdesk/public_change_language.html", {"next": return_to})
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@ from .signals import new_ticket_done, update_ticket_done
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def notify_followup_webhooks(followup):
|
def notify_followup_webhooks(followup):
|
||||||
urls = settings.HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS()
|
urls = settings.HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS()
|
||||||
if not urls:
|
if not urls:
|
||||||
@ -15,22 +16,24 @@ def notify_followup_webhooks(followup):
|
|||||||
|
|
||||||
# Serialize the ticket associated with the followup
|
# Serialize the ticket associated with the followup
|
||||||
from .serializers import TicketSerializer
|
from .serializers import TicketSerializer
|
||||||
|
|
||||||
ticket = followup.ticket
|
ticket = followup.ticket
|
||||||
ticket.set_custom_field_values()
|
ticket.set_custom_field_values()
|
||||||
serialized_ticket = TicketSerializer(ticket).data
|
serialized_ticket = TicketSerializer(ticket).data
|
||||||
|
|
||||||
# Prepare the data to send
|
# Prepare the data to send
|
||||||
data = {
|
data = {
|
||||||
'ticket': serialized_ticket,
|
"ticket": serialized_ticket,
|
||||||
'queue_slug': ticket.queue.slug,
|
"queue_slug": ticket.queue.slug,
|
||||||
'followup_id': followup.id
|
"followup_id": followup.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
for url in urls:
|
for url in urls:
|
||||||
try:
|
try:
|
||||||
requests.post(url, json=data, timeout=settings.HELPDESK_WEBHOOK_TIMEOUT)
|
requests.post(url, json=data, timeout=settings.HELPDESK_WEBHOOK_TIMEOUT)
|
||||||
except requests.exceptions.Timeout:
|
except requests.exceptions.Timeout:
|
||||||
logger.error('Timeout while sending followup webhook to %s', url)
|
logger.error("Timeout while sending followup webhook to %s", url)
|
||||||
|
|
||||||
|
|
||||||
# listener is loaded via app.py HelpdeskConfig.ready()
|
# listener is loaded via app.py HelpdeskConfig.ready()
|
||||||
@receiver(update_ticket_done)
|
@receiver(update_ticket_done)
|
||||||
@ -44,22 +47,21 @@ def send_new_ticket_webhook(ticket):
|
|||||||
return
|
return
|
||||||
# Serialize the ticket
|
# Serialize the ticket
|
||||||
from .serializers import TicketSerializer
|
from .serializers import TicketSerializer
|
||||||
|
|
||||||
ticket.set_custom_field_values()
|
ticket.set_custom_field_values()
|
||||||
serialized_ticket = TicketSerializer(ticket).data
|
serialized_ticket = TicketSerializer(ticket).data
|
||||||
|
|
||||||
# Prepare the data to send
|
# Prepare the data to send
|
||||||
data = {
|
data = {"ticket": serialized_ticket, "queue_slug": ticket.queue.slug}
|
||||||
'ticket': serialized_ticket,
|
|
||||||
'queue_slug': ticket.queue.slug
|
|
||||||
}
|
|
||||||
|
|
||||||
for url in urls:
|
for url in urls:
|
||||||
try:
|
try:
|
||||||
requests.post(url, json=data, timeout=settings.HELPDESK_WEBHOOK_TIMEOUT)
|
requests.post(url, json=data, timeout=settings.HELPDESK_WEBHOOK_TIMEOUT)
|
||||||
except requests.exceptions.Timeout:
|
except requests.exceptions.Timeout:
|
||||||
logger.error('Timeout while sending new ticket webhook to %s', url)
|
logger.error("Timeout while sending new ticket webhook to %s", url)
|
||||||
|
|
||||||
|
|
||||||
# listener is loaded via app.py HelpdeskConfig.ready()
|
# listener is loaded via app.py HelpdeskConfig.ready()
|
||||||
@receiver(new_ticket_done)
|
@receiver(new_ticket_done)
|
||||||
def send_new_ticket_webhook_receiver(sender, ticket, **kwargs):
|
def send_new_ticket_webhook_receiver(sender, ticket, **kwargs):
|
||||||
send_new_ticket_webhook(ticket)
|
send_new_ticket_webhook(ticket)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user