mirror of
https://gitea.mueller.network/extern/django-helpdesk.git
synced 2024-12-26 00:29:38 +01:00
Merge branch 'main' into view_protectors
This commit is contained in:
commit
8f9d385e10
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,6 +13,7 @@ docs/doctrees/*
|
||||
*.swp
|
||||
.idea
|
||||
.tox/**
|
||||
.venv
|
||||
|
||||
# ignore demo attachments that user might have added
|
||||
helpdesk/attachments/
|
||||
@ -25,4 +26,3 @@ demo/demodesk/media/helpdesk/attachments/DH-3/3/*
|
||||
demo/demodesk/media/helpdesk/attachments/DH-3/4/*
|
||||
!demo/demodesk/media/helpdesk/attachments/DH-3/3/someinfo.txt
|
||||
!demo/demodesk/media/helpdesk/attachments/DH-3/4/helpdesk.png
|
||||
|
||||
|
31
README.rst
31
README.rst
@ -28,11 +28,11 @@ get started with testing or developing `django-helpdesk`. The demo project
|
||||
resides in the `demo/` top-level folder.
|
||||
|
||||
It's likely that you can start up a demo project server by running
|
||||
only the command:
|
||||
only the command (consider creating a virtualenv before):
|
||||
|
||||
make rundemo
|
||||
|
||||
or with docker:
|
||||
or with docker::
|
||||
|
||||
docker build . -t demodesk
|
||||
docker run --rm -v "$PWD:/app" -p 8080:8080 demodesk
|
||||
@ -74,27 +74,33 @@ Developer Environment
|
||||
|
||||
Follow these steps to set up your development environment to contribute to helpdesk:
|
||||
- check out the helpdesk app to your local file system::
|
||||
git clone https://github.com/django-helpdesk/django-helpdesk.git
|
||||
|
||||
git clone https://github.com/django-helpdesk/django-helpdesk.git
|
||||
|
||||
- install a virtual environment
|
||||
- using virtualenv from the helpdesk base folder do::
|
||||
virtualenv .venv && source .venv/bin/activate
|
||||
- install a virtual environment and activate it::
|
||||
|
||||
python -m venv .venv && source .venv/bin/activate
|
||||
|
||||
- install the requirements for development::
|
||||
|
||||
pip install -r requirements.txt -r requirements-dev.txt
|
||||
|
||||
- install the requirements for testing as well::
|
||||
pip install -r requirements.txt -r requirements-dev.txt -r requirements-testing.txt
|
||||
- you can install the requirements for testing as well::
|
||||
|
||||
To reactivate a VENV just run:
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements-testing.txt
|
||||
|
||||
To deactivate the virtual environment, use ``deactivate``. Then to reactivate it, just run::
|
||||
|
||||
source .venv/bin/activate
|
||||
|
||||
To see option for the Makefile run: `make`
|
||||
|
||||
The project enforces a standardized formatting in the CI/CD pipeline. To ensure you have the correct formatting run::
|
||||
|
||||
make checkformat
|
||||
|
||||
To auto format any code use this::
|
||||
|
||||
make format
|
||||
|
||||
Testing
|
||||
@ -104,6 +110,9 @@ From the command line you can run the tests using: `make test`
|
||||
|
||||
See `quicktest.py` for usage details.
|
||||
|
||||
If you need to create tests for new features, add your tests in a test file to the `tests` module and call them in the test VENV with::
|
||||
python quicktest.py helpdesk.tests.test_my_new_features -v 2
|
||||
|
||||
Upgrading from previous versions
|
||||
--------------------------------
|
||||
|
||||
@ -145,5 +154,5 @@ Note that django-helpdesk is distributed with 3rd party products which
|
||||
have their own licenses. See LICENSE.3RDPARTY for license terms for
|
||||
included packages.
|
||||
|
||||
.. _note: http://docs.djangoproject.com/en/dev/ref/databases/#sqlite-string-matching
|
||||
.. _note: https://docs.djangoproject.com/en/dev/ref/databases/#substring-matching-and-case-sensitivity
|
||||
|
||||
|
@ -24,15 +24,13 @@ Ideally, you'd use a virtualenv instead
|
||||
(see below for details).
|
||||
|
||||
To use your system directory, from the top-level
|
||||
django-helpdesk directory, simply run:
|
||||
django-helpdesk directory, simply run::
|
||||
|
||||
make rundemo
|
||||
|
||||
Once the console gives a prompt that the HTTP
|
||||
server is listening, open your web browser
|
||||
and navigate to:
|
||||
|
||||
localhost:8080
|
||||
and navigate to http://localhost:8080
|
||||
|
||||
You should see the django-helpdesk public web portal!
|
||||
|
||||
@ -40,7 +38,7 @@ If you shut down the server, you can't immediately
|
||||
re-run the demo because the make commands would
|
||||
encounter problems trying to re-write the database.
|
||||
Instead, before running the demo, you will need
|
||||
to first clean the demo:
|
||||
to first clean the demo::
|
||||
|
||||
sudo make distclean
|
||||
|
||||
@ -53,7 +51,7 @@ want to use a virtualenv.
|
||||
|
||||
If so, you might change the pip in the makefile
|
||||
to point to your virtualenv's pip instead
|
||||
before running:
|
||||
before running::
|
||||
|
||||
make rundemo
|
||||
|
||||
|
@ -68,7 +68,7 @@ MIDDLEWARE = [
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'demo.demodesk.config.urls'
|
||||
ROOT_URLCONF = 'demodesk.config.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
@ -87,7 +87,7 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'demo.demodesk.config.wsgi.application'
|
||||
WSGI_APPLICATION = 'demodesk.config.wsgi.application'
|
||||
|
||||
|
||||
# django-helpdesk configuration settings
|
||||
|
@ -13,7 +13,7 @@ Before django-helpdesk will be much use, you need to do some basic configuration
|
||||
|
||||
Don't forget to set the relevant Django environment variables in your crontab::
|
||||
|
||||
*/5 * * * * /path/to/helpdesksite/manage.py get_email
|
||||
*/5 * * * * /path/to/helpdesksite/manage.py get_email
|
||||
|
||||
This will run the e-mail import every 5 minutes
|
||||
|
||||
@ -32,13 +32,13 @@ Before django-helpdesk will be much use, you need to do some basic configuration
|
||||
|
||||
4. If you wish to automatically escalate tickets based on their age, set up a cronjob to run the escalation command on a regular basis::
|
||||
|
||||
0 * * * * /path/to/helpdesksite/manage.py escalate_tickets
|
||||
0 * * * * /path/to/helpdesksite/manage.py escalate_tickets
|
||||
|
||||
This will run the escalation process hourly, using the 'Escalation Days' setting for each queue to determine which tickets to escalate.
|
||||
|
||||
5. If you wish to exclude some days (eg, weekends) from escalation calculations, enter the dates manually via the Admin, or setup a cronjob to run a management command on a regular basis::
|
||||
|
||||
0 0 * * 0 /path/to/helpdesksite/manage.py create_escalation_exclusions --days saturday,sunday --escalate-verbosely
|
||||
0 0 * * 0 /path/to/helpdesksite/manage.py create_escalation_exclusions --days saturday,sunday --escalate-verbosely
|
||||
|
||||
This will, on a weekly basis, create exclusions for the coming weekend.
|
||||
|
||||
@ -46,9 +46,9 @@ Before django-helpdesk will be much use, you need to do some basic configuration
|
||||
|
||||
7. If you do not send mail directly from your web server (eg, you need to use an SMTP server) then edit your ``settings.py`` file so it contains your mail server details::
|
||||
|
||||
EMAIL_HOST = 'XXXXX'
|
||||
EMAIL_HOST_USER = 'YYYYYY@ZZZZ.PPP'
|
||||
EMAIL_HOST_PASSWORD = '123456'
|
||||
EMAIL_HOST = 'XXXXX'
|
||||
EMAIL_HOST_USER = 'YYYYYY@ZZZZ.PPP'
|
||||
EMAIL_HOST_PASSWORD = '123456'
|
||||
|
||||
8. If you wish to use SOCKS4/5 proxy with Helpdesk Queue email operations, install PySocks manually. Please note that mixing both SOCKS and non-SOCKS email sources for different queues is only supported under Python 2; on Python 3, SOCKS proxy support is all-or-nothing: either all queue email sources must use SOCKS or none may use it. If you need this functionality on Python 3 please `let us know <https://github.com/django-helpdesk/django-helpdesk/issues/new>`_.
|
||||
|
||||
@ -88,6 +88,6 @@ You may add your own site specific navigation header to be included inside the <
|
||||
|
||||
1. Create an override template in your project's templates directory::
|
||||
|
||||
helpdesk/custom_navigation_header.html
|
||||
helpdesk/custom_navigation_header.html
|
||||
|
||||
2. Update the contents to display your custom navigation.
|
@ -3,6 +3,4 @@ Custom Fields
|
||||
|
||||
django-helpdesk supports custom fields on the ``Ticket`` model. These fields are created by using the Django administration tool, and are shown on both the public and staff submission forms. You can use most Django field types including text, integer, boolean, and list.
|
||||
|
||||
The demo at http://django-helpdesk-demo.herokuapp.com contains an example of each type of custom field, including a mix of mandatory and optional fields.
|
||||
|
||||
Custom fields are relatively inefficient; you can search them, but this might degrade performance of your installation if you make use of custom fields. They can be useful for tracking extra information that your organisation needs but that isn't supported out of the box.
|
||||
|
@ -6,4 +6,3 @@ django-helpdesk supports custom HTML templates that can be styled with CSS.
|
||||
In particular, users can include a file named `helpdesk-customize.css` in their django project directory to provide CSS overrides easily.
|
||||
|
||||
In general, entire HTML and CSS templates may be overriden by including a file of the same name in the project directory. Django automatically searches the project directory before searching for default templates included with django-helpdesk.
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
Ticket submission with embeded iframe
|
||||
-------------------------------------
|
||||
=====================================
|
||||
|
||||
Django-helpdesk associates an email address with each submitted ticket. If you integrate django-helpdesk directly into your django application, logged in users will automatically have their email address set when they visit the `/tickets/submit/` form. If you wish to pre-fill fields in this form, you can do so simply by setting the following query parameters:
|
||||
|
||||
|
@ -16,18 +16,13 @@ Contents
|
||||
settings
|
||||
spam
|
||||
custom_fields
|
||||
custom_templates
|
||||
api
|
||||
webhooks
|
||||
iframe_submission
|
||||
teams
|
||||
license
|
||||
|
||||
|
||||
How Does It Look?
|
||||
-----------------
|
||||
|
||||
You can see a demo installation at http://django-helpdesk-demo.herokuapp.com/
|
||||
|
||||
Quick Start
|
||||
-----------
|
||||
|
||||
@ -72,5 +67,3 @@ Optionally, their access to view tickets, both on the dashboard and through sear
|
||||
Licensing
|
||||
---------
|
||||
django-helpdesk is released under the BSD license, however it packages 3rd party applications which may be using a different license. More details can be found in the :doc:`license` documentation.
|
||||
|
||||
|
||||
|
137
docs/install.rst
137
docs/install.rst
@ -1,12 +1,12 @@
|
||||
.. _installation:
|
||||
Installation
|
||||
============
|
||||
|
||||
.. note::
|
||||
|
||||
For standalone installation, refer to the :doc:`standalone installation docs <standalone>`.
|
||||
For standalone installation, refer to the :doc:`standalone installation docs <standalone>`.
|
||||
|
||||
``django-helpdesk`` installation isn't difficult, but it requires you have a bit of existing know-how about Django.
|
||||
``django-helpdesk`` installation isn't difficult, but it requires you have a bit
|
||||
of existing know-how about Django.
|
||||
|
||||
|
||||
Prerequisites
|
||||
@ -33,9 +33,12 @@ Installing using PIP
|
||||
Try using ``pip install django-helpdesk``. Go and have a beer to celebrate Python packaging.
|
||||
|
||||
Checkout ``main`` branch from git (Cutting Edge)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you're planning on editing the code or just want to get whatever is the latest and greatest, you can clone the official Git repository with ``git clone git://github.com/django-helpdesk/django-helpdesk.git``. Each official release of ``django-helpdesk`` is tagged.
|
||||
If you're planning on editing the code or just want to get whatever is the latest
|
||||
and greatest, you can clone the official Git repository with
|
||||
``git clone git://github.com/django-helpdesk/django-helpdesk.git``. Each official
|
||||
release of ``django-helpdesk`` is tagged.
|
||||
|
||||
Copy the ``helpdesk`` folder into your ``PYTHONPATH`` or add it to your ``PYTHONPATH``.
|
||||
|
||||
@ -54,100 +57,134 @@ If you're on a brand new Django installation, make sure you do a ``migrate``
|
||||
errors with trying to create User settings.
|
||||
|
||||
1. Edit your ``settings.py`` file add the following entries:
|
||||
- add ``helpdesk`` to the ``INSTALLED_APPS`` along with some other required entries in the ``django.contrib`` package.
|
||||
|
||||
- Add ``helpdesk`` to the ``INSTALLED_APPS`` along with some other required
|
||||
entries in the ``django.contrib`` package.
|
||||
|
||||
An example of the core ``INSTALLED_APPS`` requirements for this app are shown below::
|
||||
An example of the core ``INSTALLED_APPS`` requirements for this app
|
||||
are shown below::
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites', # Required for determining domain url for use in emails
|
||||
'django.contrib.admin', # Required for helpdesk admin/maintenance
|
||||
'django.contrib.humanize', # Required for elapsed time formatting
|
||||
'bootstrap4form', # Required for nicer formatting of forms with the default templates
|
||||
'rest_framework', # required for the API
|
||||
'django_cleanup.apps.CleanupConfig', # Remove this if you do NOT want to delete files on the file system when the associated record is deleted in the database
|
||||
'helpdesk', # This is us!
|
||||
)
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites', # Required for determining domain url for use in emails
|
||||
'django.contrib.admin', # Required for helpdesk admin/maintenance
|
||||
'django.contrib.humanize', # Required for elapsed time formatting
|
||||
'bootstrap4form', # Required for nicer formatting of forms with the default templates
|
||||
'rest_framework', # required for the API
|
||||
'django_cleanup.apps.CleanupConfig', # Remove this if you do NOT want to delete files on the file system when the associated record is deleted in the database
|
||||
'helpdesk', # This is us!
|
||||
)
|
||||
|
||||
- Enable or disable Teams based ticket assignment by setting the boolean flag named ``HELPDESK_TEAMS_MODE_ENABLED`` to True or False.
|
||||
- Enable or disable Teams based ticket assignment by setting the boolean flag named
|
||||
``HELPDESK_TEAMS_MODE_ENABLED`` to ``True`` or ``False``.
|
||||
|
||||
IMPORTANT NOTE: It is ENABLED by default if you do not set this flag to False
|
||||
See the :doc::ref:`teams`. section for a detailed explanation of how to use teams mode.
|
||||
See the :doc:`Working with teams<teams>` section for a detailed explanation of how to
|
||||
use teams mode.
|
||||
Below is an example for disabling teams::
|
||||
HELPDESK_TEAMS_MODE_ENABLED=False
|
||||
|
||||
HELPDESK_TEAMS_MODE_ENABLED=False
|
||||
|
||||
- Your ``settings.py`` file should also define a ``SITE_ID``
|
||||
|
||||
This will allow multiple projects to share a single database, and is required by ``django.contrib.sites`` in Django 1.9+.
|
||||
If you aren't running multiple sites, you can simply add a default ``SITE_ID`` to ``settings.py``
|
||||
This will allow multiple projects to share a single database, and is
|
||||
required by ``django.contrib.sites`` in Django 1.9+.
|
||||
If you aren't running multiple sites, you can simply add a default
|
||||
``SITE_ID`` to ``settings.py``
|
||||
|
||||
Below is an example for setting a default::
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
SITE_ID = 1
|
||||
2. Make sure django-helpdesk is accessible via ``urls.py``. Add the
|
||||
following lines to ``urls.py``::
|
||||
|
||||
2. Make sure django-helpdesk is accessible via ``urls.py``. Add the following lines to ``urls.py``::
|
||||
from django.conf.urls import include
|
||||
path('helpdesk/', include('helpdesk.urls')),
|
||||
|
||||
from django.conf.urls import include
|
||||
path('helpdesk/', include('helpdesk.urls')),
|
||||
Note that you can change 'helpdesk/' to anything you like, such as
|
||||
'support/' or 'help/'. If you want django-helpdesk to be available at
|
||||
the root of your site (for example at http://support.mysite.tld/) then
|
||||
the path line will be as follows::
|
||||
|
||||
Note that you can change 'helpdesk/' to anything you like, such as 'support/' or 'help/'. If you want django-helpdesk to be available at the root of your site (for example at http://support.mysite.tld/) then the path line will be as follows::
|
||||
path('', include('helpdesk.urls', namespace='helpdesk')),
|
||||
|
||||
path('', include('helpdesk.urls', namespace='helpdesk')),
|
||||
This line will have to come *after* any other lines in your urls.py such
|
||||
as those used by the Django admin.
|
||||
|
||||
This line will have to come *after* any other lines in your urls.py such as those used by the Django admin.
|
||||
|
||||
Note that the `helpdesk` namespace is no longer required for Django 1.9+ and you can use a different namespace.
|
||||
Note that the `helpdesk` namespace is no longer required for Django 1.9+
|
||||
and you can use a different namespace.
|
||||
However, it is recommended to use the default namespace name for clarity.
|
||||
|
||||
3. Create the required database tables.
|
||||
|
||||
Migrate using Django migrations::
|
||||
|
||||
python manage.py migrate helpdesk
|
||||
python manage.py migrate helpdesk
|
||||
|
||||
4. Include your static files in your public web path::
|
||||
|
||||
python manage.py collectstatic
|
||||
python manage.py collectstatic
|
||||
|
||||
5. Inside your ``MEDIA_ROOT`` folder, inside the ``helpdesk`` folder, is a folder called ``attachments``. Ensure your web server software can write to this folder - something like this should do the trick::
|
||||
5. Inside your ``MEDIA_ROOT`` folder, inside the ``helpdesk`` folder, is a
|
||||
folder called ``attachments``. Ensure your web server software can write
|
||||
to this folder - something like this should do the trick::
|
||||
|
||||
chown www-data:www-data attachments/
|
||||
chmod 700 attachments
|
||||
chown www-data:www-data attachments/
|
||||
chmod 700 attachments
|
||||
|
||||
(substitute www-data for the user / group that your web server runs as, eg 'apache' or 'httpd')
|
||||
(substitute www-data for the user / group that your web server runs as, eg
|
||||
'apache' or 'httpd')
|
||||
|
||||
If all else fails, you could ensure all users can write to it::
|
||||
|
||||
chmod 777 attachments/
|
||||
chmod 777 attachments/
|
||||
|
||||
But this is NOT recommended, especially if you're on a shared server.
|
||||
|
||||
6. Ensure that your ``attachments`` folder has directory listings turned off, to ensure users don't download files that they are not specifically linked to from their tickets.
|
||||
6. Ensure that your ``attachments`` folder has directory listings turned off,
|
||||
to ensure users don't download files that they are not specifically linked
|
||||
to from their tickets.
|
||||
|
||||
If you are using Apache, put a ``.htaccess`` file in the ``attachments`` folder with the following content::
|
||||
If you are using Apache, put a ``.htaccess`` file in the ``attachments``
|
||||
folder with the following content::
|
||||
|
||||
Options -Indexes
|
||||
Options -Indexes
|
||||
|
||||
You will also have to make sure that ``.htaccess`` files aren't being ignored.
|
||||
|
||||
Ideally, accessing http://MEDIA_URL/helpdesk/attachments/ will give you a 403 access denied error.
|
||||
Ideally, accessing http://MEDIA_URL/helpdesk/attachments/ will give you a 403
|
||||
access denied error.
|
||||
|
||||
7. If you already have a view handling your logins, then great! If not, add the following to ``settings.py`` to get your Django installation to use the login view included in ``django-helpdesk``::
|
||||
7. If you already have a view handling your logins, then great! If not, add the
|
||||
following to ``settings.py`` to get your Django installation to use the login
|
||||
view included in ``django-helpdesk``::
|
||||
|
||||
LOGIN_URL = '/helpdesk/login/'
|
||||
LOGIN_URL = '/helpdesk/login/'
|
||||
|
||||
Alter the URL to suit your installation path.
|
||||
|
||||
8. Load initial e-mail templates, otherwise you will not be able to send e-mail::
|
||||
|
||||
python manage.py loaddata emailtemplate.json
|
||||
python manage.py loaddata emailtemplate.json
|
||||
|
||||
9. If you intend on using local mail directories for processing email into tickets, be sure to create the mail directory before adding it to the queue in the Django administrator interface. The default mail directory is ``/var/lib/mail/helpdesk/``. Ensure that the directory has appropriate permissions so that your Django/web server instance may read and write files from this directory.
|
||||
9. If you intend on using local mail directories for processing email into tickets,
|
||||
be sure to create the mail directory before adding it to the queue in the
|
||||
Django administrator interface. The default mail directory is
|
||||
``/var/lib/mail/helpdesk/``. Ensure that the directory has appropriate
|
||||
permissions so that your Django/web server instance may read and write
|
||||
files from this directory.
|
||||
|
||||
Note that by default, any mail files placed in your local directory will be permanently deleted after being successfully processed. It is strongly recommended that you take further steps to save emails if you wish to retain backups.
|
||||
Note that by default, any mail files placed in your local directory will be
|
||||
permanently deleted after being successfully processed. It is strongly recommended
|
||||
that you take further steps to save emails if you wish to retain backups.
|
||||
|
||||
Also, be aware that if a disk error occurs and the local file is not deleted, the mail may be processed multiple times and generate duplicate tickets until the file is removed. It is recommended to monitor log files for ERRORS when a file is unable to be deleted.
|
||||
Also, be aware that if a disk error occurs and the local file is not deleted,
|
||||
the mail may be processed multiple times and generate duplicate tickets until
|
||||
the file is removed. It is recommended to monitor log files for ERRORS when a
|
||||
file is unable to be deleted.
|
||||
|
||||
Upgrading from previous versions
|
||||
--------------------------------
|
||||
|
@ -1,7 +1,7 @@
|
||||
License
|
||||
=======
|
||||
|
||||
django-helpdesk is released under the terms of the BSD license. You must agree to these terms before installing or using django-helpdesk.::
|
||||
django-helpdesk is released under the terms of the BSD license. You must agree to these terms before installing or using django-helpdesk. ::
|
||||
|
||||
Copyright (c) 2008, Ross Poulton (Trading as Jutda)
|
||||
Copyright (c) 2008-2021, django-helpdesk contributors
|
||||
@ -42,6 +42,7 @@ applications are included below.
|
||||
License for jQuery & jQuery UI
|
||||
------------------------------
|
||||
::
|
||||
|
||||
Copyright (c) 2007 John Resig, http://jquery.com/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
@ -66,6 +67,7 @@ License for jQuery & jQuery UI
|
||||
License for jQuery UI 'Smoothness' theme
|
||||
----------------------------------------
|
||||
::
|
||||
|
||||
/*
|
||||
* jQuery UI screen structure and presentation
|
||||
* This CSS file was generated by ThemeRoller, a Filament Group Project for jQuery UI
|
||||
@ -76,6 +78,7 @@ License for jQuery UI 'Smoothness' theme
|
||||
License for akismet.py
|
||||
----------------------
|
||||
::
|
||||
|
||||
Copyright (c) 2003-2009, Michael Foord
|
||||
All rights reserved.
|
||||
E-mail : fuzzyman AT voidspace DOT org DOT uk
|
||||
@ -112,6 +115,7 @@ License for akismet.py
|
||||
License for jqPlot
|
||||
------------------
|
||||
::
|
||||
|
||||
Copyright (c) 2009 - 2010 Chris Leonello
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
@ -22,6 +22,7 @@ First, django-helpdesk needs ``django.core.context_processors.request`` activat
|
||||
|
||||
The following settings can be changed in your ``settings.py`` file to help change the way django-helpdesk operates. There are quite a few settings available to toggle functionality within django-helpdesk.
|
||||
|
||||
|
||||
HELPDESK_DEFAULT_SETTINGS
|
||||
-------------------------
|
||||
|
||||
@ -30,12 +31,13 @@ django-helpdesk has a built in ``UserSettings`` entity with per-user options tha
|
||||
If you want to override the default settings for your users, create ``HELPDESK_DEFAULT_SETTINGS`` as a dictionary in ``settings.py``. The default is below::
|
||||
|
||||
HELPDESK_DEFAULT_SETTINGS = {
|
||||
'use_email_as_submitter': True,
|
||||
'email_on_ticket_assign': True,
|
||||
'email_on_ticket_change': True,
|
||||
'login_view_ticketlist': True,
|
||||
'tickets_per_page': 25
|
||||
}
|
||||
'use_email_as_submitter': True,
|
||||
'email_on_ticket_assign': True,
|
||||
'email_on_ticket_change': True,
|
||||
'login_view_ticketlist': True,
|
||||
'tickets_per_page': 25
|
||||
}
|
||||
|
||||
|
||||
Access controll & Security
|
||||
---------------
|
||||
@ -119,6 +121,7 @@ These changes are visible throughout django-helpdesk
|
||||
|
||||
**Default:** ``HELPDESK_TICKETS_TIMELINE_ENABLED = True``
|
||||
|
||||
|
||||
Options shown on public pages
|
||||
-----------------------------
|
||||
|
||||
@ -159,9 +162,9 @@ Options that change ticket updates
|
||||
- **HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE** Allow non-staff users to interact with tickets?
|
||||
Set to True to allow any authenticated user to manage tickets.
|
||||
You can also apply a custom authorisation logic for identifying helpdesk staff members, by setting this to a callable.
|
||||
In that case, the value should be a function accepting the active user as a parameter and returning True if the user is considered helpdesk staff, e.g.
|
||||
In that case, the value should be a function accepting the active user as a parameter and returning True if the user is considered helpdesk staff, e.g.::
|
||||
|
||||
lambda u: u.is_authenticated() and u.is_active and u.groups.filter(name='helpdesk_staff').exists()))
|
||||
lambda u: u.is_authenticated() and u.is_active and u.groups.filter(name='helpdesk_staff').exists()
|
||||
|
||||
**Default:** ``HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = False``
|
||||
|
||||
@ -213,36 +216,36 @@ Options that change ticket properties
|
||||
|
||||
The **default** is below::
|
||||
|
||||
HELPDESK_TICKET_STATUS_CHOICES = (
|
||||
(HELPDESK_TICKET_OPEN_STATUS, _('Open')),
|
||||
(HELPDESK_TICKET_REOPENED_STATUS, _('Reopened')),
|
||||
(HELPDESK_TICKET_RESOLVED_STATUS, _('Resolved')),
|
||||
(HELPDESK_TICKET_CLOSED_STATUS, _('Closed')),
|
||||
(HELPDESK_TICKET_DUPLICATE_STATUS, _('Duplicate')),
|
||||
)
|
||||
HELPDESK_TICKET_STATUS_CHOICES = (
|
||||
(HELPDESK_TICKET_OPEN_STATUS, _('Open')),
|
||||
(HELPDESK_TICKET_REOPENED_STATUS, _('Reopened')),
|
||||
(HELPDESK_TICKET_RESOLVED_STATUS, _('Resolved')),
|
||||
(HELPDESK_TICKET_CLOSED_STATUS, _('Closed')),
|
||||
(HELPDESK_TICKET_DUPLICATE_STATUS, _('Duplicate')),
|
||||
)
|
||||
|
||||
If you wish to modify or introduce new status choices, you may add them like this::
|
||||
|
||||
# don't forget to import the gettext_lazy function at the begining of your settings file
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
# Don't forget to import the gettext_lazy function at the begining of your settings file
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# explicitly define status list integer values
|
||||
HELPDESK_TICKET_OPEN_STATUS = 1
|
||||
HELPDESK_TICKET_REOPENED_STATUS = 2
|
||||
HELPDESK_TICKET_RESOLVED_STATUS = 3
|
||||
HELPDESK_TICKET_CLOSED_STATUS = 4
|
||||
HELPDESK_TICKET_DUPLICATE_STATUS = 5
|
||||
HELPDESK_TICKET_FORKED_STATUS = 6
|
||||
# Explicitly define status list integer values
|
||||
HELPDESK_TICKET_OPEN_STATUS = 1
|
||||
HELPDESK_TICKET_REOPENED_STATUS = 2
|
||||
HELPDESK_TICKET_RESOLVED_STATUS = 3
|
||||
HELPDESK_TICKET_CLOSED_STATUS = 4
|
||||
HELPDESK_TICKET_DUPLICATE_STATUS = 5
|
||||
HELPDESK_TICKET_FORKED_STATUS = 6
|
||||
|
||||
# create the list with associated labels
|
||||
HELPDESK_TICKET_STATUS_CHOICES = (
|
||||
(HELPDESK_TICKET_OPEN_STATUS, _('Open')),
|
||||
(HELPDESK_TICKET_REOPENED_STATUS, _('Reopened')),
|
||||
(HELPDESK_TICKET_RESOLVED_STATUS, _('Resolved')),
|
||||
(HELPDESK_TICKET_CLOSED_STATUS, _('Closed')),
|
||||
(HELPDESK_TICKET_DUPLICATE_STATUS, _('Duplicate')),
|
||||
(HELPDESK_TICKET_FORKED_STATUS, _('Forked')),
|
||||
)
|
||||
# Create the list with associated labels
|
||||
HELPDESK_TICKET_STATUS_CHOICES = (
|
||||
(HELPDESK_TICKET_OPEN_STATUS, _('Open')),
|
||||
(HELPDESK_TICKET_REOPENED_STATUS, _('Reopened')),
|
||||
(HELPDESK_TICKET_RESOLVED_STATUS, _('Resolved')),
|
||||
(HELPDESK_TICKET_CLOSED_STATUS, _('Closed')),
|
||||
(HELPDESK_TICKET_DUPLICATE_STATUS, _('Duplicate')),
|
||||
(HELPDESK_TICKET_FORKED_STATUS, _('Forked')),
|
||||
)
|
||||
|
||||
- **HELPDESK_TICKET_OPEN_STATUSES** Define the list of statuses to be considered as a type of open status.
|
||||
|
||||
@ -250,56 +253,109 @@ Options that change ticket properties
|
||||
|
||||
If you have added the ``HELPDESK_TICKET_FORKED_STATUS`` status and wish to have django-helpdesk treat it as an open status choice, add it to the list of OPEN_STATUSES like this::
|
||||
|
||||
HELPDESK_TICKET_OPEN_STATUSES = (HELPDESK_TICKET_OPEN_STATUS,
|
||||
HELPDESK_TICKET_REOPENED_STATUS,
|
||||
HELPDESK_TICKET_FORKED_STATUS)
|
||||
HELPDESK_TICKET_OPEN_STATUSES = (HELPDESK_TICKET_OPEN_STATUS,
|
||||
HELPDESK_TICKET_REOPENED_STATUS,
|
||||
HELPDESK_TICKET_FORKED_STATUS)
|
||||
|
||||
- **HELPDESK_TICKET_STATUS_CHOICES_FLOW** Customize the allowed state changes depending on the current state.
|
||||
|
||||
The **default** is below::
|
||||
|
||||
HELPDESK_TICKET_STATUS_CHOICES_FLOW = {
|
||||
HELPDESK_TICKET_OPEN_STATUS: (HELPDESK_TICKET_OPEN_STATUS, HELPDESK_TICKET_RESOLVED_STATUS, HELPDESK_TICKET_CLOSED_STATUS, HELPDESK_TICKET_DUPLICATE_STATUS,),
|
||||
HELPDESK_TICKET_REOPENED_STATUS: (HELPDESK_TICKET_REOPENED_STATUS, HELPDESK_TICKET_RESOLVED_STATUS, HELPDESK_TICKET_CLOSED_STATUS, HELPDESK_TICKET_DUPLICATE_STATUS,),
|
||||
HELPDESK_TICKET_RESOLVED_STATUS: (HELPDESK_TICKET_REOPENED_STATUS, HELPDESK_TICKET_RESOLVED_STATUS, HELPDESK_TICKET_CLOSED_STATUS,),
|
||||
HELPDESK_TICKET_CLOSED_STATUS: (HELPDESK_TICKET_REOPENED_STATUS, HELPDESK_TICKET_CLOSED_STATUS,),
|
||||
HELPDESK_TICKET_DUPLICATE_STATUS: (HELPDESK_TICKET_REOPENED_STATUS, HELPDESK_TICKET_DUPLICATE_STATUS,),
|
||||
}
|
||||
HELPDESK_TICKET_STATUS_CHOICES_FLOW = {
|
||||
HELPDESK_TICKET_OPEN_STATUS: (HELPDESK_TICKET_OPEN_STATUS, HELPDESK_TICKET_RESOLVED_STATUS, HELPDESK_TICKET_CLOSED_STATUS, HELPDESK_TICKET_DUPLICATE_STATUS,),
|
||||
HELPDESK_TICKET_REOPENED_STATUS: (HELPDESK_TICKET_REOPENED_STATUS, HELPDESK_TICKET_RESOLVED_STATUS, HELPDESK_TICKET_CLOSED_STATUS, HELPDESK_TICKET_DUPLICATE_STATUS,),
|
||||
HELPDESK_TICKET_RESOLVED_STATUS: (HELPDESK_TICKET_REOPENED_STATUS, HELPDESK_TICKET_RESOLVED_STATUS, HELPDESK_TICKET_CLOSED_STATUS,),
|
||||
HELPDESK_TICKET_CLOSED_STATUS: (HELPDESK_TICKET_REOPENED_STATUS, HELPDESK_TICKET_CLOSED_STATUS,),
|
||||
HELPDESK_TICKET_DUPLICATE_STATUS: (HELPDESK_TICKET_REOPENED_STATUS, HELPDESK_TICKET_DUPLICATE_STATUS,),
|
||||
}
|
||||
|
||||
If you wish to modify or have introduce new status choices, you may configure their status change flow like this::
|
||||
|
||||
# adding HELPDESK_TICKET_FORKED_STATUS to the other allowed states flow and defining its own flow
|
||||
HELPDESK_TICKET_STATUS_CHOICES_FLOW = {
|
||||
HELPDESK_TICKET_OPEN_STATUS: (HELPDESK_TICKET_OPEN_STATUS, HELPDESK_TICKET_FORKED_STATUS, HELPDESK_TICKET_RESOLVED_STATUS, HELPDESK_TICKET_CLOSED_STATUS, HELPDESK_TICKET_DUPLICATE_STATUS,),
|
||||
HELPDESK_TICKET_REOPENED_STATUS: (HELPDESK_TICKET_REOPENED_STATUS, HELPDESK_TICKET_FORKED_STATUS, HELPDESK_TICKET_RESOLVED_STATUS, HELPDESK_TICKET_CLOSED_STATUS, HELPDESK_TICKET_DUPLICATE_STATUS,),
|
||||
HELPDESK_TICKET_RESOLVED_STATUS: (HELPDESK_TICKET_REOPENED_STATUS, HELPDESK_TICKET_RESOLVED_STATUS, HELPDESK_TICKET_CLOSED_STATUS,),
|
||||
HELPDESK_TICKET_CLOSED_STATUS: (HELPDESK_TICKET_REOPENED_STATUS, HELPDESK_TICKET_CLOSED_STATUS,),
|
||||
HELPDESK_TICKET_DUPLICATE_STATUS: (HELPDESK_TICKET_REOPENED_STATUS, HELPDESK_TICKET_DUPLICATE_STATUS,),
|
||||
HELPDESK_TICKET_FORKED_STATUS: (HELPDESK_TICKET_OPEN_STATUS, HELPDESK_TICKET_FORKED_STATUS, HELPDESK_TICKET_RESOLVED_STATUS, HELPDESK_TICKET_CLOSED_STATUS, HELPDESK_TICKET_DUPLICATE_STATUS,),
|
||||
}
|
||||
# Adding HELPDESK_TICKET_FORKED_STATUS to the other allowed states flow and defining its own flow
|
||||
HELPDESK_TICKET_STATUS_CHOICES_FLOW = {
|
||||
HELPDESK_TICKET_OPEN_STATUS: (HELPDESK_TICKET_OPEN_STATUS, HELPDESK_TICKET_FORKED_STATUS, HELPDESK_TICKET_RESOLVED_STATUS, HELPDESK_TICKET_CLOSED_STATUS, HELPDESK_TICKET_DUPLICATE_STATUS,),
|
||||
HELPDESK_TICKET_REOPENED_STATUS: (HELPDESK_TICKET_REOPENED_STATUS, HELPDESK_TICKET_FORKED_STATUS, HELPDESK_TICKET_RESOLVED_STATUS, HELPDESK_TICKET_CLOSED_STATUS, HELPDESK_TICKET_DUPLICATE_STATUS,),
|
||||
HELPDESK_TICKET_RESOLVED_STATUS: (HELPDESK_TICKET_REOPENED_STATUS, HELPDESK_TICKET_RESOLVED_STATUS, HELPDESK_TICKET_CLOSED_STATUS,),
|
||||
HELPDESK_TICKET_CLOSED_STATUS: (HELPDESK_TICKET_REOPENED_STATUS, HELPDESK_TICKET_CLOSED_STATUS,),
|
||||
HELPDESK_TICKET_DUPLICATE_STATUS: (HELPDESK_TICKET_REOPENED_STATUS, HELPDESK_TICKET_DUPLICATE_STATUS,),
|
||||
HELPDESK_TICKET_FORKED_STATUS: (HELPDESK_TICKET_OPEN_STATUS, HELPDESK_TICKET_FORKED_STATUS, HELPDESK_TICKET_RESOLVED_STATUS, HELPDESK_TICKET_CLOSED_STATUS, HELPDESK_TICKET_DUPLICATE_STATUS,),
|
||||
}
|
||||
|
||||
- **HELPDESK_TICKET_PRIORITY_CHOICES** Customize the priority choices for all tickets.
|
||||
|
||||
The **default** is below::
|
||||
|
||||
HELPDESK_TICKET_PRIORITY_CHOICES = (
|
||||
(1, _('1. Critical')),
|
||||
(2, _('2. High')),
|
||||
(3, _('3. Normal')),
|
||||
(4, _('4. Low')),
|
||||
(5, _('5. Very Low')),
|
||||
)
|
||||
HELPDESK_TICKET_PRIORITY_CHOICES = (
|
||||
(1, _('1. Critical')),
|
||||
(2, _('2. High')),
|
||||
(3, _('3. Normal')),
|
||||
(4, _('4. Low')),
|
||||
(5, _('5. Very Low')),
|
||||
)
|
||||
|
||||
If you have a new instance, you may override those settings but if you want to keep previous tickets priorities and add new choices, you may increment integer values like this::
|
||||
|
||||
HELPDESK_TICKET_PRIORITY_CHOICES = (
|
||||
(1, _('1. Critical')),
|
||||
(2, _('2. High')),
|
||||
(3, _('3. Normal')),
|
||||
(4, _('4. Low')),
|
||||
(5, _('5. Very Low')),
|
||||
(6, _('6. Cold')),
|
||||
(7, _('7. Hot')),
|
||||
)
|
||||
HELPDESK_TICKET_PRIORITY_CHOICES = (
|
||||
(1, _('1. Critical')),
|
||||
(2, _('2. High')),
|
||||
(3, _('3. Normal')),
|
||||
(4, _('4. Low')),
|
||||
(5, _('5. Very Low')),
|
||||
(6, _('6. Cold')),
|
||||
(7, _('7. Hot')),
|
||||
)
|
||||
|
||||
|
||||
Time Tracking Options
|
||||
---------------------
|
||||
|
||||
- **HELPDESK_FOLLOWUP_TIME_SPENT_AUTO** If ``True``, calculate follow-up 'time_spent' with previous follow-up or ticket creation time.
|
||||
|
||||
**Default:** ``HELPDESK_FOLLOWUP_TIME_SPENT_AUTO = False``
|
||||
|
||||
- **HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS** If defined, calculates follow-up 'time_spent' according to open hours.
|
||||
|
||||
**Default:** ``HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS = {}``
|
||||
|
||||
If HELPDESK_FOLLOWUP_TIME_SPENT_AUTO is ``True``, you may set open hours to remove off hours from 'time_spent'::
|
||||
|
||||
HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS = {
|
||||
"monday": (8.5, 19),
|
||||
"tuesday": (8.5, 19),
|
||||
"wednesday": (8.5, 19),
|
||||
"thursday": (8.5, 19),
|
||||
"friday": (8.5, 19),
|
||||
"saturday": (0, 0),
|
||||
"sunday": (0, 0),
|
||||
}
|
||||
|
||||
Valid hour values must be set between 0 and 23.9999.
|
||||
In this example 8.5 is interpreted as 8:30AM, saturdays and sundays don't count.
|
||||
|
||||
- **HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS** List of days in format "%Y-%m-%d" to exclude from automatic follow-up 'time_spent' calculation.
|
||||
|
||||
**Default:** ``HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = ()``
|
||||
|
||||
This example removes Christmas and New Year's Eve in 2024::
|
||||
|
||||
HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = ("2024-12-25", "2024-12-31",)
|
||||
|
||||
- **HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES** List of ticket statuses to exclude from automatic follow-up 'time_spent' calculation.
|
||||
|
||||
**Default:** ``HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = ()``
|
||||
|
||||
This example will have follow-ups to resolved ticket status not to be counted in::
|
||||
|
||||
HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = (HELPDESK_TICKET_RESOLVED_STATUS,)
|
||||
|
||||
- **HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES** List of ticket queues slugs to exclude from automatic follow-up 'time_spent' calculation.
|
||||
|
||||
**Default:** ``HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ()``
|
||||
|
||||
This example will have follow-ups excluded from time calculation if they belong to the queue with slug ``time-not-counting-queue``::
|
||||
|
||||
HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ('time-not-counting-queue',)
|
||||
|
||||
|
||||
Staff Ticket Creation Settings
|
||||
------------------------------
|
||||
@ -308,6 +364,7 @@ Staff Ticket Creation Settings
|
||||
|
||||
**Default:** ``HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = False``
|
||||
|
||||
|
||||
Staff Ticket View Settings
|
||||
------------------------------
|
||||
|
||||
@ -316,7 +373,6 @@ Staff Ticket View Settings
|
||||
**Default:** ``HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = False``
|
||||
|
||||
|
||||
|
||||
Default E-Mail Settings
|
||||
-----------------------
|
||||
|
||||
@ -328,6 +384,7 @@ The following settings default to ``None`` but can be set as defaults, rather th
|
||||
- ``QUEUE_EMAIL_BOX_USER``
|
||||
- ``QUEUE_EMAIL_BOX_PASSWORD``
|
||||
|
||||
|
||||
Discontinued Settings
|
||||
---------------------
|
||||
|
||||
|
@ -22,11 +22,10 @@ This service is free to use, within their terms and conditions.
|
||||
|
||||
If you have either of these settings enabled, the spam filtering will be done automatically. If you have *both* settings configured, TypePad will be used instead of Akismet.
|
||||
|
||||
|
||||
Example
|
||||
~~~~~~~
|
||||
|
||||
A sample configuration in ``settings.py`` may be::
|
||||
|
||||
TYPEPAD_ANTISPAM_API_KEY = 'abc123'
|
||||
TYPEPAD_ANTISPAM_API_KEY = 'abc123'
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
==============================
|
||||
Django-Helpdesk Standalone Installation
|
||||
==============================
|
||||
=======================================
|
||||
|
||||
Installation
|
||||
------------
|
||||
@ -9,25 +8,25 @@ Installation
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone git@github.com:django-helpdesk/django-helpdesk.git
|
||||
git clone git@github.com:django-helpdesk/django-helpdesk.git
|
||||
|
||||
2. Go to the standalone helpdesk installation directory:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
cd django-helpdesk/standalone
|
||||
cd django-helpdesk/standalone
|
||||
|
||||
3. Execute the installation script:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./setup.sh
|
||||
./setup.sh
|
||||
|
||||
4. Start the services:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker-compose up
|
||||
docker-compose up
|
||||
|
||||
Creating an Admin User
|
||||
----------------------
|
||||
@ -36,25 +35,25 @@ Creating an Admin User
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker ps
|
||||
docker ps
|
||||
|
||||
2. Execute into the `standalone-django-helpdesk-1` container:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker exec -it standalone-django-helpdesk-1 bash
|
||||
docker exec -it standalone-django-helpdesk-1 bash
|
||||
|
||||
3. Change directory to the application's root:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
cd /opt/django-helpdesk/standalone
|
||||
cd /opt/django-helpdesk/standalone
|
||||
|
||||
4. Create a superuser:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python3 manage.py createsuperuser
|
||||
python3 manage.py createsuperuser
|
||||
|
||||
5. Visit `localhost:80` in your browser to access the server. Navigate to the `/admin` URL to set up new users. Ensure to configure the "Site" in the admin section for ticket email URLs to function correctly.
|
||||
|
||||
@ -71,16 +70,16 @@ Configuration for Production Use
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<style>
|
||||
.navbar-brand {
|
||||
background: url("https://www.libertyaces.com/files/liberty-logo.png") no-repeat;
|
||||
background-size: auto;
|
||||
width: 320px;
|
||||
background-size: contain;
|
||||
height: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.navbar-brand {
|
||||
background: url("https://www.libertyaces.com/files/liberty-logo.png") no-repeat;
|
||||
background-size: auto;
|
||||
width: 320px;
|
||||
background-size: contain;
|
||||
height: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
AWS SES Email Configuration
|
||||
---------------------------
|
||||
@ -89,23 +88,22 @@ An example `local_settings` configuration for utilizing AWS SES for email:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from .settings import *
|
||||
import os
|
||||
|
||||
from .settings import *
|
||||
import os
|
||||
|
||||
DEFAULT_FROM_EMAIL = "support@bitswan.space"
|
||||
SERVER_EMAIL = "support@bitswan.space"
|
||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID")
|
||||
EMAIL_BACKEND = "django_ses.SESBackend"
|
||||
AWS_SES_REGION_NAME = "eu-west-1"
|
||||
AWS_SES_REGION_ENDPOINT = "email.eu-west-1.amazonaws.com"
|
||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")
|
||||
DEFAULT_FROM_EMAIL = "support@bitswan.space"
|
||||
SERVER_EMAIL = "support@bitswan.space"
|
||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID")
|
||||
EMAIL_BACKEND = "django_ses.SESBackend"
|
||||
AWS_SES_REGION_NAME = "eu-west-1"
|
||||
AWS_SES_REGION_ENDPOINT = "email.eu-west-1.amazonaws.com"
|
||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")
|
||||
|
||||
To integrate `django-ses`, bindmount a file to `/opt/extra-dependencies.txt` containing:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
django-ses
|
||||
django-ses
|
||||
|
||||
Make sure you update the `docker.env` file with the necessary secrets.
|
||||
|
||||
@ -127,10 +125,9 @@ Working from the previous SES example we add the following to `local_settings`:
|
||||
|
||||
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
||||
|
||||
|
||||
To integrate `django-ses`, bindmount a file to `/opt/extra-dependencies.txt` containing:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
django-storages
|
||||
boto3
|
||||
django-storages
|
||||
boto3
|
||||
|
@ -1,4 +1,5 @@
|
||||
.. _teams:
|
||||
|
||||
Working with teams and larger organizations
|
||||
===========================================
|
||||
|
||||
@ -11,6 +12,7 @@ If you are embedding the helpdesk app into your own apps, it is possible that th
|
||||
|
||||
How It Works
|
||||
------------
|
||||
|
||||
Rather than assigning tickets to teams directly, django-helpdesk allows you assign tickets to knowledge-base items and then assign knowledge base items to teams.
|
||||
|
||||
Knowledge-base items can be in either public or private knowledge-base categories, so this organizational structure need not have any influence on the external appearance of your public helpdesk web portal.
|
||||
@ -21,22 +23,28 @@ You can assign a knowledge-base item to a team on the Helpdesk admin page.
|
||||
|
||||
Once you have set up teams. Unassigned tickets which are associated with a knowledge-base item will only be shown on the dashboard to those users who are members of the team which is associated with that knowledge-base item.
|
||||
|
||||
|
||||
Implementing Custom Teams Functionality
|
||||
--------------------------------
|
||||
---------------------------------------
|
||||
|
||||
If you want to use a different team app or implement your own team based app, you can hook it into Helpdesk using the following 3 settings:
|
||||
|
||||
``HELPDESK_TEAMS_MODEL``: point this to the model that defines a team in your custom implementation
|
||||
``HELPDESK_TEAMS_MIGRATION_DEPENDENCIES``: set this to an array of migration(s) that are required to have run that will ensure the link that will be added as defined in the HELPDESK_TEAMS_MODEL will be available as a model to Helpdesk
|
||||
``HELPDESK_KBITEM_TEAM_GETTER``: the method that will be called that must return a list of users who belong to a given team
|
||||
|
||||
|
||||
Configuring Teams Functionality
|
||||
-----------------------------
|
||||
-------------------------------
|
||||
|
||||
Teams functionality is enabled by default but can be disabled using this entry in your ``settings.py``::
|
||||
HELPDESK_TEAMS_MODE_ENABLED=False
|
||||
|
||||
HELPDESK_TEAMS_MODE_ENABLED=False
|
||||
|
||||
If you do not disable teams functionality then you must add additional apps into the ``INSTALLED_APPS`` in your ``settings.py``.
|
||||
The following can be pasted into your settings.py BELOW the ``INSTALLED_APPS`` definition::
|
||||
INSTALLED_APPS.extend([
|
||||
|
||||
INSTALLED_APPS.extend([
|
||||
'account', # Required by pinax-teams
|
||||
'pinax.invitations', # required by pinax-teams
|
||||
'pinax.teams', # team support
|
||||
@ -45,7 +53,10 @@ INSTALLED_APPS.extend([
|
||||
|
||||
Alternatively just add the 4 apps listed above into the ``INSTALLED_APPS``.
|
||||
|
||||
|
||||
Disabling Teams Functionality
|
||||
-----------------------------
|
||||
|
||||
Teams functionality is enabled by default but can be disabled using this entry in your ``settings.py``::
|
||||
HELPDESK_TEAMS_MODE_ENABLED=False
|
||||
|
||||
HELPDESK_TEAMS_MODE_ENABLED=False
|
||||
|
@ -3,7 +3,6 @@ Upgrading
|
||||
|
||||
Your ``django-helpdesk`` installation can be upgraded to the latest version using the release notes below.
|
||||
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
||||
@ -22,13 +21,13 @@ The tips below are based on modifications of the original installation instructi
|
||||
|
||||
- Under `INSTALLED_APPS`, `bootstrapform` needs to be replaced with `bootstrap4form`
|
||||
|
||||
- Unless turning off `pinax_teams`, add the following to `INSTALLED_APPS` for `pinax_teams`:
|
||||
```
|
||||
"account",
|
||||
"pinax.invitations",
|
||||
"pinax.teams",
|
||||
"reversion",
|
||||
```
|
||||
- Unless turning off `pinax_teams`, add the following to `INSTALLED_APPS` for `pinax_teams`::
|
||||
|
||||
"account",
|
||||
"pinax.invitations",
|
||||
"pinax.teams",
|
||||
"reversion",
|
||||
|
||||
|
||||
- If using `send_templated_mail`, then it now needs to be imported from `helpdesk.templated_email`
|
||||
|
||||
|
@ -9,7 +9,7 @@ lib.py - Common functions (eg multipart e-mail)
|
||||
|
||||
from datetime import date, datetime, time
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import ValidationError, ImproperlyConfigured
|
||||
from django.utils.encoding import smart_str
|
||||
from helpdesk.settings import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT
|
||||
import logging
|
||||
@ -173,11 +173,10 @@ def format_time_spent(time_spent):
|
||||
"""Format time_spent attribute to "[H]HHh:MMm" text string to be allign in
|
||||
all graphical outputs
|
||||
"""
|
||||
|
||||
if time_spent:
|
||||
time_spent = "{0:02d}h:{1:02d}m".format(
|
||||
time_spent.seconds // 3600,
|
||||
time_spent.seconds // 60
|
||||
int(time_spent.total_seconds()) // 3600,
|
||||
int(time_spent.total_seconds()) % 3600 // 60
|
||||
)
|
||||
else:
|
||||
time_spent = ""
|
||||
@ -194,3 +193,47 @@ def convert_value(value):
|
||||
return value.strftime(CUSTOMFIELD_TIME_FORMAT)
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def daily_time_spent_calculation(earliest, latest, open_hours):
|
||||
"""Returns the number of seconds for a single day time interval according to open hours."""
|
||||
|
||||
time_spent_seconds = 0
|
||||
|
||||
# avoid rendering day in different locale
|
||||
weekday = ('monday', 'tuesday', 'wednesday', 'thursday',
|
||||
'friday', 'saturday', 'sunday')[earliest.weekday()]
|
||||
|
||||
# enforce correct settings
|
||||
MIDNIGHT = 23.9999
|
||||
start, end = open_hours.get(weekday, (0, MIDNIGHT))
|
||||
if not 0 <= start <= end <= MIDNIGHT:
|
||||
raise ImproperlyConfigured("HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS"
|
||||
f" setting for {weekday} out of (0, 23.9999) boundary")
|
||||
|
||||
# transform decimals to minutes and seconds
|
||||
start_hour, start_minute, start_second = int(start), int(start % 1 * 60), int(start * 60 % 1 * 60)
|
||||
end_hour, end_minute, end_second = int(end), int(end % 1 * 60), int(end * 60 % 1 * 60)
|
||||
|
||||
# translate time for delta calculation
|
||||
earliest_f = earliest.hour + earliest.minute / 60 + earliest.second / 3600
|
||||
latest_f = latest.hour + latest.minute / 60 + latest.second / (60 * 60) + latest.microsecond / (60 * 60 * 999999)
|
||||
|
||||
# if latest time is midnight and close hour is midnight, add a second to the time spent
|
||||
if latest_f >= MIDNIGHT and end == MIDNIGHT:
|
||||
time_spent_seconds += 1
|
||||
|
||||
if earliest_f < start:
|
||||
earliest = earliest.replace(hour=start_hour, minute=start_minute, second=start_second)
|
||||
elif earliest_f >= end:
|
||||
earliest = earliest.replace(hour=end_hour, minute=end_minute, second=end_second)
|
||||
|
||||
if latest_f < start:
|
||||
latest = latest.replace(hour=start_hour, minute=start_minute, second=start_second)
|
||||
elif latest_f >= end:
|
||||
latest = latest.replace(hour=end_hour, minute=end_minute, second=end_second)
|
||||
|
||||
day_delta = latest - earliest
|
||||
time_spent_seconds += day_delta.seconds
|
||||
|
||||
return time_spent_seconds
|
@ -8,7 +8,7 @@ models.py - Model (and hence database) definitions. This is the core of the
|
||||
"""
|
||||
|
||||
|
||||
from .lib import convert_value
|
||||
from .lib import format_time_spent, convert_value, daily_time_spent_calculation
|
||||
from .templated_email import send_templated_mail
|
||||
from .validators import validate_file_extension
|
||||
from .webhooks import send_new_ticket_webhook
|
||||
@ -33,17 +33,6 @@ from rest_framework import serializers
|
||||
import uuid
|
||||
|
||||
|
||||
def format_time_spent(time_spent):
|
||||
if time_spent:
|
||||
time_spent = "{0:02d}h:{1:02d}m".format(
|
||||
time_spent.seconds // 3600,
|
||||
time_spent.seconds % 3600 // 60
|
||||
)
|
||||
else:
|
||||
time_spent = ""
|
||||
return time_spent
|
||||
|
||||
|
||||
class EscapeHtml(Extension):
|
||||
def extendMarkdown(self, md):
|
||||
md.preprocessors.deregister('html_block')
|
||||
@ -1000,9 +989,12 @@ class FollowUp(models.Model):
|
||||
return u"%s#followup%s" % (self.ticket.get_absolute_url(), self.id)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
t = self.ticket
|
||||
t.modified = timezone.now()
|
||||
t.save()
|
||||
self.ticket.modified = timezone.now()
|
||||
self.ticket.save()
|
||||
|
||||
if helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO and not self.time_spent:
|
||||
self.time_spent = self.time_spent_calculation()
|
||||
|
||||
super(FollowUp, self).save(*args, **kwargs)
|
||||
|
||||
def get_markdown(self):
|
||||
@ -1012,6 +1004,85 @@ class FollowUp(models.Model):
|
||||
def time_spent_formated(self):
|
||||
return format_time_spent(self.time_spent)
|
||||
|
||||
def time_spent_calculation(self):
|
||||
"Returns timedelta according to rules settings."
|
||||
|
||||
open_hours = helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS
|
||||
holidays = helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS
|
||||
exclude_statuses = helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES
|
||||
exclude_queues = helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES
|
||||
|
||||
# queryset for this ticket previous follow-ups
|
||||
prev_fup_qs = self.ticket.followup_set.all()
|
||||
if self.id:
|
||||
# if the follow-up exist in DB, only keep previous follow-ups
|
||||
prev_fup_qs = prev_fup_qs.filter(date__lt=self.date)
|
||||
|
||||
# handle exclusions
|
||||
|
||||
# extract previous status from follow-up or ticket for exclusion check
|
||||
if exclude_statuses:
|
||||
try:
|
||||
prev_fup = prev_fup_qs.latest("date")
|
||||
prev_status = prev_fup.new_status
|
||||
except ObjectDoesNotExist:
|
||||
prev_status = self.ticket.status
|
||||
|
||||
# don't calculate status exclusions
|
||||
if prev_status in exclude_statuses:
|
||||
return datetime.timedelta(seconds=0)
|
||||
|
||||
# find the previous queue for exclusion check
|
||||
if exclude_queues:
|
||||
try:
|
||||
prev_fup_ids = prev_fup_qs.values_list('id', flat=True)
|
||||
prev_queue_change = TicketChange.objects.filter(followup_id__in=prev_fup_ids,
|
||||
field=_('Queue')).latest('id')
|
||||
prev_queue = Queue.objects.get(pk=prev_queue_change.new_value)
|
||||
prev_queue_slug = prev_queue.slug
|
||||
except ObjectDoesNotExist:
|
||||
prev_queue_slug = self.ticket.queue.slug
|
||||
|
||||
# don't calculate queue exclusions
|
||||
if prev_queue_slug in exclude_queues:
|
||||
return datetime.timedelta(seconds=0)
|
||||
|
||||
# no exclusion found
|
||||
|
||||
time_spent_seconds = 0
|
||||
|
||||
# extract earliest from previous follow-up or ticket
|
||||
try:
|
||||
prev_fup = prev_fup_qs.latest("date")
|
||||
earliest = prev_fup.date
|
||||
except ObjectDoesNotExist:
|
||||
earliest = self.ticket.created
|
||||
|
||||
# latest time is current follow-up date
|
||||
latest = self.date
|
||||
|
||||
# split time interval by days
|
||||
days = latest.toordinal() - earliest.toordinal()
|
||||
for day in range(days + 1):
|
||||
if day == 0:
|
||||
start_day_time = earliest
|
||||
if days == 0:
|
||||
# close single day case
|
||||
end_day_time = latest
|
||||
else:
|
||||
end_day_time = earliest.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
elif day == days:
|
||||
start_day_time = latest.replace(hour=0, minute=0, second=0)
|
||||
end_day_time = latest
|
||||
else:
|
||||
middle_day_time = earliest + datetime.timedelta(days=day)
|
||||
start_day_time = middle_day_time.replace(hour=0, minute=0, second=0)
|
||||
end_day_time = middle_day_time.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
|
||||
if start_day_time.strftime("%Y-%m-%d") not in holidays:
|
||||
time_spent_seconds += daily_time_spent_calculation(start_day_time, end_day_time, open_hours)
|
||||
|
||||
return datetime.timedelta(seconds=time_spent_seconds)
|
||||
|
||||
class TicketChange(models.Model):
|
||||
"""
|
||||
|
@ -160,6 +160,36 @@ TICKET_PRIORITY_CHOICES = getattr(settings,
|
||||
'HELPDESK_TICKET_PRIORITY_CHOICES',
|
||||
DEFAULT_TICKET_PRIORITY_CHOICES)
|
||||
|
||||
|
||||
#########################
|
||||
# time tracking options #
|
||||
#########################
|
||||
|
||||
# Follow-ups automatic time_spent calculation
|
||||
FOLLOWUP_TIME_SPENT_AUTO = getattr(settings,
|
||||
'HELPDESK_FOLLOWUP_TIME_SPENT_AUTO',
|
||||
False)
|
||||
|
||||
# Calculate time_spent according to open hours
|
||||
FOLLOWUP_TIME_SPENT_OPENING_HOURS = getattr(settings,
|
||||
'HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS',
|
||||
{})
|
||||
|
||||
# Holidays don't count for time_spent calculation
|
||||
FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = getattr(settings,
|
||||
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS',
|
||||
())
|
||||
|
||||
# Time doesn't count for listed ticket statuses
|
||||
FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = getattr(settings,
|
||||
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES',
|
||||
())
|
||||
|
||||
# Time doesn't count for listed queues slugs
|
||||
FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = getattr(settings,
|
||||
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES',
|
||||
())
|
||||
|
||||
############################
|
||||
# options for public pages #
|
||||
############################
|
||||
|
@ -150,6 +150,9 @@
|
||||
<dt><label for='id_priority'>{% trans "Priority" %}</label></dt>
|
||||
<dd><select id='id_priority' name='priority'>{% for p in priorities %}{% if p.0 == ticket.priority %}<option value='{{ p.0 }}' selected='selected'>{{ p.1 }}</option>{% else %}<option value='{{ p.0 }}'>{{ p.1 }}</option>{% endif %}{% endfor %}</select></dd>
|
||||
|
||||
<dt><label for='id_queue'>{% trans "Queue" %}</label></dt>
|
||||
<dd><select id='id_queue' name='queue'>{% for queue_id, queue_name in queues %}<option value='{{ queue_id }}'{% if queue_id == ticket.queue.id %} selected{% endif %}>{{ queue_name }}</option>{% endfor %}</select></dd>
|
||||
|
||||
<dt><label for='id_due_date'>{% trans "Due on" %}</label></dt>
|
||||
<dd>{{ form.due_date }}</dd>
|
||||
|
||||
|
@ -7,6 +7,7 @@ from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.models import CustomField, Queue, Ticket
|
||||
from helpdesk.templatetags.ticket_to_link import num_to_link
|
||||
from helpdesk.user import HelpdeskUser
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
try: # python 3
|
||||
@ -323,3 +324,46 @@ class TicketActionsTestCase(TestCase):
|
||||
ticket_1_follow_up, ticket_2_follow_up])
|
||||
self.assertEqual(list(ticket_1.ticketcc_set.all()),
|
||||
[ticket_1_cc, ticket_2_cc])
|
||||
|
||||
def test_update_ticket_queue(self):
|
||||
"""Tests whether user can change the queue in the Respond to this ticket section."""
|
||||
|
||||
# log user in
|
||||
self.loginUser()
|
||||
|
||||
# create ticket
|
||||
initial_data = {
|
||||
'title': 'Queue change ticket test',
|
||||
'queue': self.queue_public,
|
||||
'assigned_to': self.user,
|
||||
'status': Ticket.OPEN_STATUS,
|
||||
}
|
||||
ticket = Ticket.objects.create(**initial_data)
|
||||
ticket_id = ticket.id
|
||||
|
||||
# initial queue
|
||||
self.assertEqual(ticket.queue, self.queue_public)
|
||||
|
||||
# POST first follow-up with new queue
|
||||
new_queue = Queue.objects.create(
|
||||
title='New Queue',
|
||||
slug='newqueue',
|
||||
)
|
||||
post_data = {
|
||||
'comment': 'first follow-up in new queue',
|
||||
'queue': str(new_queue.id),
|
||||
}
|
||||
response = self.client.post(reverse('helpdesk:update',
|
||||
kwargs={'ticket_id': ticket_id}),
|
||||
post_data)
|
||||
|
||||
# queue was correctly modified
|
||||
ticket.refresh_from_db()
|
||||
self.assertEqual(ticket.queue, new_queue)
|
||||
|
||||
# ticket change was saved
|
||||
latest_fup = ticket.followup_set.latest('date')
|
||||
latest_ticketchange = latest_fup.ticketchange_set.latest('id')
|
||||
self.assertEqual(latest_ticketchange.field, _('Queue'))
|
||||
self.assertEqual(int(latest_ticketchange.old_value), self.queue_public.id)
|
||||
self.assertEqual(int(latest_ticketchange.new_value), new_queue.id)
|
325
helpdesk/tests/test_time_spent_auto.py
Normal file
325
helpdesk/tests/test_time_spent_auto.py
Normal file
@ -0,0 +1,325 @@
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase, override_settings
|
||||
from django.test.client import Client
|
||||
from django.urls import reverse
|
||||
from helpdesk.models import FollowUp, Queue, Ticket
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
import uuid
|
||||
|
||||
|
||||
@override_settings(USE_TZ=True)
|
||||
class TimeSpentAutoTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Creates a queue, ticket and user."""
|
||||
self.queue_public = Queue.objects.create(
|
||||
title='Queue 1',
|
||||
slug='q1',
|
||||
allow_public_submission=True,
|
||||
dedicated_time=timedelta(minutes=60)
|
||||
)
|
||||
|
||||
self.ticket_data = dict(queue=self.queue_public,
|
||||
title='test ticket',
|
||||
description='test ticket description')
|
||||
|
||||
self.user = User.objects.create(
|
||||
username='staff',
|
||||
email='staff@example.com',
|
||||
password=make_password('Test1234'),
|
||||
is_staff=True,
|
||||
is_superuser=False,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
self.client = Client()
|
||||
|
||||
|
||||
def loginUser(self, is_staff=True):
|
||||
"""Create a staff user and login"""
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create(
|
||||
username='User_1',
|
||||
is_staff=is_staff,
|
||||
)
|
||||
self.user.set_password('pass')
|
||||
self.user.save()
|
||||
self.client.login(username='User_1', password='pass')
|
||||
|
||||
|
||||
def test_add_two_followups_time_spent_auto(self):
|
||||
"""Tests automatic time_spent calculation."""
|
||||
# activate automatic calculation
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True
|
||||
|
||||
# ticket creation date, follow-up creation date, assertion value
|
||||
TEST_VALUES = (
|
||||
# friday
|
||||
('2024-03-01T00:00:00+00:00', '2024-03-01T09:30:10+00:00', timedelta(hours=9, minutes=30, seconds=10)),
|
||||
('2024-03-01T00:00:00+00:00', '2024-03-01T23:59:58+00:00', timedelta(hours=23, minutes=59, seconds=58)),
|
||||
('2024-03-01T00:00:00+00:00', '2024-03-01T23:59:59+00:00', timedelta(hours=23, minutes=59, seconds=59)),
|
||||
('2024-03-01T00:00:00+00:00', '2024-03-02T00:00:00+00:00', timedelta(hours=24)),
|
||||
('2024-03-01T00:00:00+00:00', '2024-03-02T09:00:00+00:00', timedelta(hours=33)),
|
||||
('2024-03-01T00:00:00+00:00', '2024-03-03T00:00:00+00:00', timedelta(hours=48)),
|
||||
)
|
||||
|
||||
for (ticket_time, fup_time, assertion_delta) in TEST_VALUES:
|
||||
# create and setup test ticket time
|
||||
ticket = Ticket.objects.create(**self.ticket_data)
|
||||
ticket_time_p = datetime.strptime(ticket_time, "%Y-%m-%dT%H:%M:%S%z")
|
||||
ticket.created = ticket_time_p
|
||||
ticket.modified = ticket_time_p
|
||||
ticket.save()
|
||||
|
||||
fup_time_p = datetime.strptime(fup_time, "%Y-%m-%dT%H:%M:%S%z")
|
||||
followup1 = FollowUp.objects.create(
|
||||
ticket=ticket,
|
||||
date=fup_time_p,
|
||||
title="Testing followup",
|
||||
comment="Testing followup time spent",
|
||||
public=True,
|
||||
user=self.user,
|
||||
new_status=1,
|
||||
message_id=uuid.uuid4().hex,
|
||||
time_spent=None
|
||||
)
|
||||
|
||||
self.assertEqual(followup1.time_spent.total_seconds(), assertion_delta.total_seconds())
|
||||
self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds())
|
||||
|
||||
# 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)):
|
||||
|
||||
followup2 = FollowUp.objects.create(
|
||||
ticket=ticket,
|
||||
date=followup1.date + delta,
|
||||
title="Testing followup 2",
|
||||
comment="Testing followup time spent 2",
|
||||
public=True,
|
||||
user=self.user,
|
||||
new_status=1,
|
||||
message_id=uuid.uuid4().hex,
|
||||
time_spent=None
|
||||
)
|
||||
|
||||
self.assertEqual(followup2.time_spent.total_seconds(), delta.total_seconds())
|
||||
self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds() + delta.total_seconds())
|
||||
|
||||
# delete second follow-up as we test it with many intervals
|
||||
followup2.delete()
|
||||
|
||||
|
||||
def test_followup_time_spent_auto_opening_hours(self):
|
||||
"""Tests automatic time_spent calculation with opening hours and holidays."""
|
||||
|
||||
# activate automatic calculation
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS = {
|
||||
"monday": (0, 23.9999),
|
||||
"tuesday": (8, 18),
|
||||
"wednesday": (8.5, 18.5),
|
||||
"thursday": (0, 10),
|
||||
"friday": (13, 23),
|
||||
"saturday": (0, 0),
|
||||
"sunday": (0, 0),
|
||||
}
|
||||
|
||||
# adding holidays
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = (
|
||||
'2024-03-18', '2024-03-19', '2024-03-20', '2024-03-21', '2024-03-22',
|
||||
)
|
||||
|
||||
# ticket creation date, follow-up creation date, assertion value
|
||||
TEST_VALUES = (
|
||||
# monday
|
||||
('2024-03-04T00:00:00+00:00', '2024-03-04T09:30:10+00:00', timedelta(hours=9, minutes=30, seconds=10)),
|
||||
# tuesday
|
||||
('2024-03-05T07:00:00+00:00', '2024-03-05T09:00:00+00:00', timedelta(hours=1)),
|
||||
('2024-03-05T17:50:00+00:00', '2024-03-05T17:51:00+00:00', timedelta(minutes=1)),
|
||||
('2024-03-05T17:50:00+00:00', '2024-03-05T19:51:00+00:00', timedelta(minutes=10)),
|
||||
('2024-03-05T18:00:00+00:00', '2024-03-05T23:59:59+00:00', timedelta(hours=0)),
|
||||
('2024-03-05T20:00:00+00:00', '2024-03-05T20:59:59+00:00', timedelta(hours=0)),
|
||||
# wednesday
|
||||
('2024-03-06T08:00:00+00:00', '2024-03-06T09:01:00+00:00', timedelta(minutes=31)),
|
||||
('2024-03-06T01:00:00+00:00', '2024-03-06T19:30:10+00:00', timedelta(hours=10)),
|
||||
('2024-03-06T18:01:00+00:00', '2024-03-06T19:00:00+00:00', timedelta(minutes=29)),
|
||||
# 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)),
|
||||
# friday
|
||||
('2024-03-08T00:00:00+00:00', '2024-03-08T23:30:10+00:00', timedelta(hours=10)),
|
||||
# saturday
|
||||
('2024-03-09T00:00:00+00:00', '2024-03-09T09:30:10+00:00', timedelta(hours=0)),
|
||||
# sunday
|
||||
('2024-03-10T00:00:00+00:00', '2024-03-10T09:30:10+00:00', timedelta(hours=0)),
|
||||
|
||||
# monday to sunday
|
||||
('2024-03-04T04:00:00+00:00', '2024-03-10T09:00:00+00:00', timedelta(hours=60)),
|
||||
|
||||
# two weeks
|
||||
('2024-03-04T04:00:00+00:00', '2024-03-17T09:00:00+00:00', timedelta(hours=124)),
|
||||
|
||||
# three weeks, the third one is holidays
|
||||
('2024-03-04T04:00:00+00:00', '2024-03-24T09:00:00+00:00', timedelta(hours=124)),
|
||||
('2024-03-18T04:00:00+00:00', '2024-03-24T09:00:00+00:00', timedelta(hours=0)),
|
||||
)
|
||||
|
||||
for (ticket_time, fup_time, assertion_delta) in TEST_VALUES:
|
||||
# create and setup test ticket time
|
||||
ticket = Ticket.objects.create(**self.ticket_data)
|
||||
ticket_time_p = datetime.strptime(ticket_time, "%Y-%m-%dT%H:%M:%S%z")
|
||||
ticket.created = ticket_time_p
|
||||
ticket.modified = ticket_time_p
|
||||
ticket.save()
|
||||
|
||||
fup_time_p = datetime.strptime(fup_time, "%Y-%m-%dT%H:%M:%S%z")
|
||||
followup1 = FollowUp.objects.create(
|
||||
ticket=ticket,
|
||||
date=fup_time_p,
|
||||
title="Testing followup",
|
||||
comment="Testing followup time spent",
|
||||
public=True,
|
||||
user=self.user,
|
||||
new_status=1,
|
||||
message_id=uuid.uuid4().hex,
|
||||
time_spent=None
|
||||
)
|
||||
|
||||
self.assertEqual(followup1.time_spent.total_seconds(), assertion_delta.total_seconds())
|
||||
self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds())
|
||||
|
||||
# removing opening hours and holidays
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS = {}
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = ()
|
||||
|
||||
def test_followup_time_spent_auto_exclude_statuses(self):
|
||||
"""Tests automatic time_spent calculation OPEN_STATUS exclusion."""
|
||||
|
||||
# activate automatic calculation
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True
|
||||
|
||||
# Follow-ups with OPEN_STATUS are excluded from time counting
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = (Ticket.OPEN_STATUS,)
|
||||
|
||||
|
||||
# create and setup test ticket time
|
||||
ticket = Ticket.objects.create(**self.ticket_data)
|
||||
ticket_time_p = datetime.strptime('2024-03-04T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z")
|
||||
ticket.created = ticket_time_p
|
||||
ticket.modified = ticket_time_p
|
||||
ticket.save()
|
||||
|
||||
fup_time_p = datetime.strptime('2024-03-10T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z")
|
||||
followup1 = FollowUp.objects.create(
|
||||
ticket=ticket,
|
||||
date=fup_time_p,
|
||||
title="Testing followup",
|
||||
comment="Testing followup time spent",
|
||||
public=True,
|
||||
user=self.user,
|
||||
new_status=1,
|
||||
message_id=uuid.uuid4().hex,
|
||||
time_spent=None
|
||||
)
|
||||
|
||||
# The Follow-up time_spent should be zero as the default OPEN_STATUS was excluded from calculation
|
||||
self.assertEqual(followup1.time_spent.total_seconds(), 0.0)
|
||||
self.assertEqual(ticket.time_spent.total_seconds(), 0.0)
|
||||
|
||||
# Remove status exclusion
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = ()
|
||||
|
||||
|
||||
def test_followup_time_spent_auto_exclude_queues(self):
|
||||
"""Tests automatic time_spent calculation queues exclusion."""
|
||||
|
||||
# activate automatic calculation
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True
|
||||
|
||||
# Follow-ups within the default queue are excluded from time counting
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ('q1',)
|
||||
|
||||
|
||||
# create and setup test ticket time
|
||||
ticket = Ticket.objects.create(**self.ticket_data)
|
||||
ticket_time_p = datetime.strptime('2024-03-04T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z")
|
||||
ticket.created = ticket_time_p
|
||||
ticket.modified = ticket_time_p
|
||||
ticket.save()
|
||||
|
||||
fup_time_p = datetime.strptime('2024-03-10T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z")
|
||||
followup1 = FollowUp.objects.create(
|
||||
ticket=ticket,
|
||||
date=fup_time_p,
|
||||
title="Testing followup",
|
||||
comment="Testing followup time spent",
|
||||
public=True,
|
||||
user=self.user,
|
||||
new_status=1,
|
||||
message_id=uuid.uuid4().hex,
|
||||
time_spent=None
|
||||
)
|
||||
|
||||
# The Follow-up time_spent should be zero as the default queue was excluded from calculation
|
||||
self.assertEqual(followup1.time_spent.total_seconds(), 0.0)
|
||||
self.assertEqual(ticket.time_spent.total_seconds(), 0.0)
|
||||
|
||||
# Remove queues exclusion
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ()
|
||||
|
||||
def test_http_followup_time_spent_auto_exclude_queues(self):
|
||||
"""Tests automatic time_spent calculation queues exclusion with client"""
|
||||
|
||||
# activate automatic calculation
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ('stop1', 'stop2')
|
||||
|
||||
# make staff user
|
||||
self.loginUser()
|
||||
|
||||
# create queues
|
||||
queues_sequence = ('new', 'stop1', 'resume1', 'stop2', 'resume2', 'end')
|
||||
queues = dict()
|
||||
for slug in queues_sequence:
|
||||
queues[slug] = Queue.objects.create(
|
||||
title=slug,
|
||||
slug=slug,
|
||||
)
|
||||
|
||||
# create ticket
|
||||
initial_data = {
|
||||
'title': 'Queue change ticket test',
|
||||
'queue': queues['new'],
|
||||
'assigned_to': self.user,
|
||||
'status': Ticket.OPEN_STATUS,
|
||||
'created': datetime.strptime('2024-04-09T08:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z")
|
||||
}
|
||||
ticket = Ticket.objects.create(**initial_data)
|
||||
|
||||
# create a change queue follow-up every hour
|
||||
# first follow-up created at the same time of the ticket without queue change
|
||||
# new --1h--> stop1 --0h--> resume1 --1h--> stop2 --0h--> resume2 --1h--> end
|
||||
for (i, queue) in enumerate(queues_sequence):
|
||||
# create follow-up
|
||||
post_data = {
|
||||
'comment': 'ticket in queue {}'.format(queue),
|
||||
'queue': queues[queue].id,
|
||||
}
|
||||
response = self.client.post(reverse('helpdesk:update', kwargs={
|
||||
'ticket_id': ticket.id}), post_data)
|
||||
latest_fup = ticket.followup_set.latest('id')
|
||||
latest_fup.date = ticket.created + timedelta(hours=i)
|
||||
latest_fup.time_spent = None
|
||||
latest_fup.save()
|
||||
|
||||
# total ticket time for followups is 5 hours
|
||||
self.assertEqual(latest_fup.date - ticket.created, timedelta(hours=5))
|
||||
# calculated time spent with 2 hours exclusion is 3 hours
|
||||
self.assertEqual(ticket.time_spent.total_seconds(), timedelta(hours=3).total_seconds())
|
||||
|
||||
# remove queues exclusion
|
||||
helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ()
|
@ -200,6 +200,7 @@ def update_ticket(
|
||||
owner=-1,
|
||||
ticket_title=None,
|
||||
priority=-1,
|
||||
queue=-1,
|
||||
new_status=None,
|
||||
time_spent=None,
|
||||
due_date=None,
|
||||
@ -213,6 +214,8 @@ def update_ticket(
|
||||
title = ticket.title
|
||||
if priority == -1:
|
||||
priority = ticket.priority
|
||||
if queue == -1:
|
||||
queue = ticket.queue.id
|
||||
if new_status is None:
|
||||
new_status = ticket.status
|
||||
if new_checklists is None:
|
||||
@ -302,6 +305,14 @@ def update_ticket(
|
||||
c.save()
|
||||
ticket.priority = priority
|
||||
|
||||
if queue != ticket.queue.id:
|
||||
c = f.ticketchange_set.create(
|
||||
field=_('Queue'),
|
||||
old_value=ticket.queue.id,
|
||||
new_value=queue,
|
||||
)
|
||||
ticket.queue_id = queue
|
||||
|
||||
if due_date != ticket.due_date:
|
||||
c = TicketChange(
|
||||
followup=f,
|
||||
|
@ -428,6 +428,7 @@ def view_ticket(request, ticket_id):
|
||||
'form': form,
|
||||
'active_users': users,
|
||||
'priorities': Ticket.PRIORITY_CHOICES,
|
||||
'queues': queue_choices,
|
||||
'preset_replies': PreSetReply.objects.filter(
|
||||
Q(queues=ticket.queue) | Q(queues__isnull=True)),
|
||||
'ticketcc_string': ticketcc_string,
|
||||
@ -566,6 +567,7 @@ def update_ticket_view(request, ticket_id, public=False):
|
||||
title = request.POST.get('title', '')
|
||||
owner = int(request.POST.get('owner', -1))
|
||||
priority = int(request.POST.get('priority', ticket.priority))
|
||||
queue = int(request.POST.get('queue', ticket.queue.id))
|
||||
|
||||
# Check if a change happened on checklists
|
||||
new_checklists = {}
|
||||
@ -589,6 +591,7 @@ def update_ticket_view(request, ticket_id, public=False):
|
||||
new_status == ticket.status,
|
||||
title == ticket.title,
|
||||
priority == int(ticket.priority),
|
||||
queue == int(ticket.queue.id),
|
||||
due_date == ticket.due_date,
|
||||
(owner == -1) or (not owner and not ticket.assigned_to) or
|
||||
(owner and User.objects.get(id=owner) == ticket.assigned_to),
|
||||
@ -605,6 +608,7 @@ def update_ticket_view(request, ticket_id, public=False):
|
||||
public = request.POST.get('public', False),
|
||||
owner = int(request.POST.get('owner', -1)),
|
||||
priority = int(request.POST.get('priority', -1)),
|
||||
queue = int(request.POST.get('queue', -1)),
|
||||
new_status = new_status,
|
||||
time_spent = get_time_spent_from_request(request),
|
||||
due_date = get_due_date_from_request_or_ticket(request, ticket),
|
||||
|
Loading…
Reference in New Issue
Block a user