mirror of
https://gitea.mueller.network/extern/django-helpdesk.git
synced 2024-12-27 00:59:00 +01:00
add a new field to KBItem model to keep track of all users who voted, and make changes to the vote function so that it checks whether a user has already votred
This commit is contained in:
parent
0f99599982
commit
e0c03996ad
0
build/lib/helpdesk/__init__.py
Normal file
0
build/lib/helpdesk/__init__.py
Normal file
75
build/lib/helpdesk/admin.py
Normal file
75
build/lib/helpdesk/admin.py
Normal file
@ -0,0 +1,75 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from helpdesk.models import Queue, Ticket, FollowUp, PreSetReply, KBCategory
|
||||
from helpdesk.models import EscalationExclusion, EmailTemplate, KBItem
|
||||
from helpdesk.models import TicketChange, Attachment, IgnoreEmail
|
||||
from helpdesk.models import CustomField
|
||||
|
||||
|
||||
@admin.register(Queue)
|
||||
class QueueAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'slug', 'email_address', 'locale')
|
||||
prepopulated_fields = {"slug": ("title",)}
|
||||
|
||||
|
||||
@admin.register(Ticket)
|
||||
class TicketAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'status', 'assigned_to', 'queue', 'hidden_submitter_email',)
|
||||
date_hierarchy = 'created'
|
||||
list_filter = ('queue', 'assigned_to', 'status')
|
||||
|
||||
def hidden_submitter_email(self, ticket):
|
||||
if ticket.submitter_email:
|
||||
username, domain = ticket.submitter_email.split("@")
|
||||
username = username[:2] + "*" * (len(username) - 2)
|
||||
domain = domain[:1] + "*" * (len(domain) - 2) + domain[-1:]
|
||||
return "%s@%s" % (username, domain)
|
||||
else:
|
||||
return ticket.submitter_email
|
||||
hidden_submitter_email.short_description = _('Submitter E-Mail')
|
||||
|
||||
|
||||
class TicketChangeInline(admin.StackedInline):
|
||||
model = TicketChange
|
||||
|
||||
|
||||
class AttachmentInline(admin.StackedInline):
|
||||
model = Attachment
|
||||
|
||||
|
||||
@admin.register(FollowUp)
|
||||
class FollowUpAdmin(admin.ModelAdmin):
|
||||
inlines = [TicketChangeInline, AttachmentInline]
|
||||
list_display = ('ticket_get_ticket_for_url', 'title', 'date', 'ticket', 'user', 'new_status')
|
||||
list_filter = ('user', 'date', 'new_status')
|
||||
|
||||
def ticket_get_ticket_for_url(self, obj):
|
||||
return obj.ticket.ticket_for_url
|
||||
ticket_get_ticket_for_url.short_description = _('Slug')
|
||||
|
||||
|
||||
@admin.register(KBItem)
|
||||
class KBItemAdmin(admin.ModelAdmin):
|
||||
list_display = ('category', 'title', 'last_updated',)
|
||||
list_display_links = ('title',)
|
||||
|
||||
|
||||
@admin.register(CustomField)
|
||||
class CustomFieldAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'label', 'data_type')
|
||||
|
||||
|
||||
@admin.register(EmailTemplate)
|
||||
class EmailTemplateAdmin(admin.ModelAdmin):
|
||||
list_display = ('template_name', 'heading', 'locale')
|
||||
list_filter = ('locale', )
|
||||
|
||||
|
||||
@admin.register(IgnoreEmail)
|
||||
class IgnoreEmailAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'queue_list', 'email_address', 'keep_in_mailbox')
|
||||
|
||||
|
||||
admin.site.register(PreSetReply)
|
||||
admin.site.register(EscalationExclusion)
|
||||
admin.site.register(KBCategory)
|
373
build/lib/helpdesk/akismet.py
Normal file
373
build/lib/helpdesk/akismet.py
Normal file
@ -0,0 +1,373 @@
|
||||
# Version 0.2.0
|
||||
# 2009/06/18
|
||||
|
||||
# Copyright Michael Foord 2005-2009
|
||||
# akismet.py
|
||||
# Python interface to the akismet API
|
||||
# E-mail fuzzyman@voidspace.org.uk
|
||||
|
||||
# http://www.voidspace.org.uk/python/modules.shtml
|
||||
# http://akismet.com
|
||||
|
||||
# Released subject to the BSD License
|
||||
# See http://www.voidspace.org.uk/python/license.shtml
|
||||
|
||||
# Updated by django-helpdesk developers, 2018
|
||||
# to be compatible with python 3
|
||||
|
||||
|
||||
"""
|
||||
A python interface to the `Akismet <http://akismet.com>`_ API.
|
||||
This is a web service for blocking SPAM comments to blogs - or other online
|
||||
services.
|
||||
|
||||
You will need a Wordpress API key, from `wordpress.com <http://wordpress.com>`_.
|
||||
|
||||
You should pass in the keyword argument 'agent' to the name of your program,
|
||||
when you create an Akismet instance. This sets the ``user-agent`` to a useful
|
||||
value.
|
||||
|
||||
The default is::
|
||||
|
||||
Python Interface by Fuzzyman | akismet.py/0.2.0
|
||||
|
||||
Whatever you pass in, will replace the *Python Interface by Fuzzyman* part.
|
||||
**0.2.0** will change with the version of this interface.
|
||||
|
||||
Usage example::
|
||||
|
||||
from akismet import Akismet
|
||||
|
||||
api = Akismet(agent='Test Script')
|
||||
# if apikey.txt is in place,
|
||||
# the key will automatically be set
|
||||
# or you can call api.setAPIKey()
|
||||
#
|
||||
if api.key is None:
|
||||
print >> sys.stderr, "No 'apikey.txt' file."
|
||||
elif not api.verify_key():
|
||||
print >> sys.stderr, "The API key is invalid."
|
||||
else:
|
||||
# data should be a dictionary of values
|
||||
# They can all be filled in with defaults
|
||||
# from a CGI environment
|
||||
if api.comment_check(comment, data):
|
||||
print >> sys.stderr, 'This comment is spam.'
|
||||
else:
|
||||
print >> sys.stderr, 'This comment is ham.'
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
try:
|
||||
from urllib import urlencode # python2
|
||||
except ImportError:
|
||||
from urllib.parse import urlencode # python3
|
||||
|
||||
import socket
|
||||
if hasattr(socket, 'setdefaulttimeout'):
|
||||
# Set the default timeout on sockets to 5 seconds
|
||||
socket.setdefaulttimeout(5)
|
||||
|
||||
__version__ = '0.2.0'
|
||||
|
||||
__all__ = (
|
||||
'__version__',
|
||||
'Akismet',
|
||||
'AkismetError',
|
||||
'APIKeyError',
|
||||
)
|
||||
|
||||
__author__ = 'Michael Foord <fuzzyman AT voidspace DOT org DOT uk>'
|
||||
|
||||
__docformat__ = "restructuredtext en"
|
||||
|
||||
user_agent = "%s | akismet.py/%s"
|
||||
DEFAULTAGENT = 'Python Interface by Fuzzyman/%s'
|
||||
|
||||
isfile = os.path.isfile
|
||||
|
||||
urllib2 = None
|
||||
try:
|
||||
from google.appengine.api import urlfetch
|
||||
except ImportError:
|
||||
import urllib2
|
||||
|
||||
if urllib2 is None:
|
||||
def _fetch_url(url, data, headers):
|
||||
req = urlfetch.fetch(url=url, payload=data, method=urlfetch.POST, headers=headers)
|
||||
if req.status_code == 200:
|
||||
return req.content
|
||||
raise Exception('Could not fetch Akismet URL: %s Response code: %s' %
|
||||
(url, req.status_code))
|
||||
else:
|
||||
def _fetch_url(url, data, headers):
|
||||
req = urllib2.Request(url, data, headers)
|
||||
h = urllib2.urlopen(req)
|
||||
resp = h.read()
|
||||
return resp
|
||||
|
||||
|
||||
class AkismetError(Exception):
|
||||
"""Base class for all akismet exceptions."""
|
||||
pass
|
||||
|
||||
|
||||
class APIKeyError(AkismetError):
|
||||
"""Invalid API key."""
|
||||
pass
|
||||
|
||||
|
||||
class Akismet(object):
|
||||
"""A class for working with the akismet API"""
|
||||
|
||||
baseurl = 'rest.akismet.com/1.1/'
|
||||
|
||||
def __init__(self, key=None, blog_url=None, agent=None):
|
||||
"""Automatically calls ``setAPIKey``."""
|
||||
if agent is None:
|
||||
agent = DEFAULTAGENT % __version__
|
||||
self.user_agent = user_agent % (agent, __version__)
|
||||
self.setAPIKey(key, blog_url)
|
||||
|
||||
def _getURL(self):
|
||||
"""
|
||||
Fetch the url to make requests to.
|
||||
|
||||
This comprises of api key plus the baseurl.
|
||||
"""
|
||||
return 'http://%s.%s' % (self.key, self.baseurl)
|
||||
|
||||
def _safeRequest(self, url, data, headers):
|
||||
try:
|
||||
resp = _fetch_url(url, data, headers)
|
||||
except Exception as e:
|
||||
raise AkismetError(str(e))
|
||||
return resp
|
||||
|
||||
def setAPIKey(self, key=None, blog_url=None):
|
||||
"""
|
||||
Set the wordpress API key for all transactions.
|
||||
|
||||
If you don't specify an explicit API ``key`` and ``blog_url`` it will
|
||||
attempt to load them from a file called ``apikey.txt`` in the current
|
||||
directory.
|
||||
|
||||
This method is *usually* called automatically when you create a new
|
||||
``Akismet`` instance.
|
||||
"""
|
||||
if key is None and isfile('apikey.txt'):
|
||||
the_file = [l.strip() for l in open('apikey.txt').readlines()
|
||||
if l.strip() and not l.strip().startswith('#')]
|
||||
try:
|
||||
self.key = the_file[0]
|
||||
self.blog_url = the_file[1]
|
||||
except IndexError:
|
||||
raise APIKeyError("Your 'apikey.txt' is invalid.")
|
||||
else:
|
||||
self.key = key
|
||||
self.blog_url = blog_url
|
||||
|
||||
def verify_key(self):
|
||||
"""
|
||||
This equates to the ``verify-key`` call against the akismet API.
|
||||
|
||||
It returns ``True`` if the key is valid.
|
||||
|
||||
The docs state that you *ought* to call this at the start of the
|
||||
transaction.
|
||||
|
||||
It raises ``APIKeyError`` if you have not yet set an API key.
|
||||
|
||||
If the connection to akismet fails, it allows the normal ``HTTPError``
|
||||
or ``URLError`` to be raised.
|
||||
(*akismet.py* uses `urllib2 <http://docs.python.org/lib/module-urllib2.html>`_)
|
||||
"""
|
||||
if self.key is None:
|
||||
raise APIKeyError("Your have not set an API key.")
|
||||
data = {'key': self.key, 'blog': self.blog_url}
|
||||
# this function *doesn't* use the key as part of the URL
|
||||
url = 'http://%sverify-key' % self.baseurl
|
||||
# we *don't* trap the error here
|
||||
# so if akismet is down it will raise an HTTPError or URLError
|
||||
headers = {'User-Agent': self.user_agent}
|
||||
resp = self._safeRequest(url, urlencode(data), headers)
|
||||
if resp.lower() == 'valid':
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _build_data(self, comment, data):
|
||||
"""
|
||||
This function builds the data structure required by ``comment_check``,
|
||||
``submit_spam``, and ``submit_ham``.
|
||||
|
||||
It modifies the ``data`` dictionary you give it in place. (and so
|
||||
doesn't return anything)
|
||||
|
||||
It raises an ``AkismetError`` if the user IP or user-agent can't be
|
||||
worked out.
|
||||
"""
|
||||
data['comment_content'] = comment
|
||||
if 'user_ip' not in data:
|
||||
try:
|
||||
val = os.environ['REMOTE_ADDR']
|
||||
except KeyError:
|
||||
raise AkismetError("No 'user_ip' supplied")
|
||||
data['user_ip'] = val
|
||||
if 'user_agent' not in data:
|
||||
try:
|
||||
val = os.environ['HTTP_USER_AGENT']
|
||||
except KeyError:
|
||||
raise AkismetError("No 'user_agent' supplied")
|
||||
data['user_agent'] = val
|
||||
#
|
||||
data.setdefault('referrer', os.environ.get('HTTP_REFERER', 'unknown'))
|
||||
data.setdefault('permalink', '')
|
||||
data.setdefault('comment_type', 'comment')
|
||||
data.setdefault('comment_author', '')
|
||||
data.setdefault('comment_author_email', '')
|
||||
data.setdefault('comment_author_url', '')
|
||||
data.setdefault('SERVER_ADDR', os.environ.get('SERVER_ADDR', ''))
|
||||
data.setdefault('SERVER_ADMIN', os.environ.get('SERVER_ADMIN', ''))
|
||||
data.setdefault('SERVER_NAME', os.environ.get('SERVER_NAME', ''))
|
||||
data.setdefault('SERVER_PORT', os.environ.get('SERVER_PORT', ''))
|
||||
data.setdefault('SERVER_SIGNATURE', os.environ.get('SERVER_SIGNATURE', ''))
|
||||
data.setdefault('SERVER_SOFTWARE', os.environ.get('SERVER_SOFTWARE', ''))
|
||||
data.setdefault('HTTP_ACCEPT', os.environ.get('HTTP_ACCEPT', ''))
|
||||
data.setdefault('blog', self.blog_url)
|
||||
|
||||
def comment_check(self, comment, data=None, build_data=True, DEBUG=False):
|
||||
"""
|
||||
This is the function that checks comments.
|
||||
|
||||
It returns ``True`` for spam and ``False`` for ham.
|
||||
|
||||
If you set ``DEBUG=True`` then it will return the text of the response,
|
||||
instead of the ``True`` or ``False`` object.
|
||||
|
||||
It raises ``APIKeyError`` if you have not yet set an API key.
|
||||
|
||||
If the connection to Akismet fails then the ``HTTPError`` or
|
||||
``URLError`` will be propogated.
|
||||
|
||||
As a minimum it requires the body of the comment. This is the
|
||||
``comment`` argument.
|
||||
|
||||
Akismet requires some other arguments, and allows some optional ones.
|
||||
The more information you give it, the more likely it is to be able to
|
||||
make an accurate diagnosise.
|
||||
|
||||
You supply these values using a mapping object (dictionary) as the
|
||||
``data`` argument.
|
||||
|
||||
If ``build_data`` is ``True`` (the default), then *akismet.py* will
|
||||
attempt to fill in as much information as possible, using default
|
||||
values where necessary. This is particularly useful for programs
|
||||
running in a {acro;CGI} environment. A lot of useful information
|
||||
can be supplied from evironment variables (``os.environ``). See below.
|
||||
|
||||
You *only* need supply values for which you don't want defaults filled
|
||||
in for. All values must be strings.
|
||||
|
||||
There are a few required values. If they are not supplied, and
|
||||
defaults can't be worked out, then an ``AkismetError`` is raised.
|
||||
|
||||
If you set ``build_data=False`` and a required value is missing an
|
||||
``AkismetError`` will also be raised.
|
||||
|
||||
The normal values (and defaults) are as follows : ::
|
||||
|
||||
'user_ip': os.environ['REMOTE_ADDR'] (*)
|
||||
'user_agent': os.environ['HTTP_USER_AGENT'] (*)
|
||||
'referrer': os.environ.get('HTTP_REFERER', 'unknown') [#]_
|
||||
'permalink': ''
|
||||
'comment_type': 'comment' [#]_
|
||||
'comment_author': ''
|
||||
'comment_author_email': ''
|
||||
'comment_author_url': ''
|
||||
'SERVER_ADDR': os.environ.get('SERVER_ADDR', '')
|
||||
'SERVER_ADMIN': os.environ.get('SERVER_ADMIN', '')
|
||||
'SERVER_NAME': os.environ.get('SERVER_NAME', '')
|
||||
'SERVER_PORT': os.environ.get('SERVER_PORT', '')
|
||||
'SERVER_SIGNATURE': os.environ.get('SERVER_SIGNATURE', '')
|
||||
'SERVER_SOFTWARE': os.environ.get('SERVER_SOFTWARE', '')
|
||||
'HTTP_ACCEPT': os.environ.get('HTTP_ACCEPT', '')
|
||||
|
||||
(*) Required values
|
||||
|
||||
You may supply as many additional 'HTTP_*' type values as you wish.
|
||||
These should correspond to the http headers sent with the request.
|
||||
|
||||
.. [#] Note the spelling "referrer". This is a required value by the
|
||||
akismet api - however, referrer information is not always
|
||||
supplied by the browser or server. In fact the HTTP protocol
|
||||
forbids relying on referrer information for functionality in
|
||||
programs.
|
||||
.. [#] The `API docs <http://akismet.com/development/api/>`_ state that this value
|
||||
can be " *blank, comment, trackback, pingback, or a made up value*
|
||||
*like 'registration'* ".
|
||||
"""
|
||||
if self.key is None:
|
||||
raise APIKeyError("Your have not set an API key.")
|
||||
if data is None:
|
||||
data = {}
|
||||
if build_data:
|
||||
self._build_data(comment, data)
|
||||
if 'blog' not in data:
|
||||
data['blog'] = self.blog_url
|
||||
url = '%scomment-check' % self._getURL()
|
||||
# we *don't* trap the error here
|
||||
# so if akismet is down it will raise an HTTPError or URLError
|
||||
headers = {'User-Agent': self.user_agent}
|
||||
resp = self._safeRequest(url, urlencode(data), headers)
|
||||
if DEBUG:
|
||||
return resp
|
||||
resp = resp.lower()
|
||||
if resp == 'true':
|
||||
return True
|
||||
elif resp == 'false':
|
||||
return False
|
||||
else:
|
||||
# NOTE: Happens when you get a 'howdy wilbur' response !
|
||||
raise AkismetError('missing required argument.')
|
||||
|
||||
def submit_spam(self, comment, data=None, build_data=True):
|
||||
"""
|
||||
This function is used to tell akismet that a comment it marked as ham,
|
||||
is really spam.
|
||||
|
||||
It takes all the same arguments as ``comment_check``, except for
|
||||
*DEBUG*.
|
||||
"""
|
||||
if self.key is None:
|
||||
raise APIKeyError("Your have not set an API key.")
|
||||
if data is None:
|
||||
data = {}
|
||||
if build_data:
|
||||
self._build_data(comment, data)
|
||||
url = '%ssubmit-spam' % self._getURL()
|
||||
# we *don't* trap the error here
|
||||
# so if akismet is down it will raise an HTTPError or URLError
|
||||
headers = {'User-Agent': self.user_agent}
|
||||
self._safeRequest(url, urlencode(data), headers)
|
||||
|
||||
def submit_ham(self, comment, data=None, build_data=True):
|
||||
"""
|
||||
This function is used to tell akismet that a comment it marked as spam,
|
||||
is really ham.
|
||||
|
||||
It takes all the same arguments as ``comment_check``, except for
|
||||
*DEBUG*.
|
||||
"""
|
||||
if self.key is None:
|
||||
raise APIKeyError("Your have not set an API key.")
|
||||
if data is None:
|
||||
data = {}
|
||||
if build_data:
|
||||
self._build_data(comment, data)
|
||||
url = '%ssubmit-ham' % self._getURL()
|
||||
# we *don't* trap the error here
|
||||
# so if akismet is down it will raise an HTTPError or URLError
|
||||
headers = {'User-Agent': self.user_agent}
|
||||
self._safeRequest(url, urlencode(data), headers)
|
6
build/lib/helpdesk/apps.py
Normal file
6
build/lib/helpdesk/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class HelpdeskConfig(AppConfig):
|
||||
name = 'helpdesk'
|
||||
verbose_name = "Helpdesk"
|
23
build/lib/helpdesk/decorators.py
Normal file
23
build/lib/helpdesk/decorators.py
Normal file
@ -0,0 +1,23 @@
|
||||
from functools import wraps
|
||||
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponseRedirect, Http404
|
||||
from django.utils.decorators import available_attrs
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
|
||||
|
||||
def protect_view(view_func):
|
||||
"""
|
||||
Decorator for protecting the views checking user, redirecting
|
||||
to the log-in page if necessary or returning 404 status code
|
||||
"""
|
||||
@wraps(view_func, assigned=available_attrs(view_func))
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
if not request.user.is_authenticated and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT:
|
||||
return HttpResponseRedirect(reverse('helpdesk:login'))
|
||||
elif not request.user.is_authenticated and helpdesk_settings.HELPDESK_ANON_ACCESS_RAISES_404:
|
||||
raise Http404
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
return _wrapped_view
|
1349
build/lib/helpdesk/fixtures/emailtemplate.json
Normal file
1349
build/lib/helpdesk/fixtures/emailtemplate.json
Normal file
File diff suppressed because it is too large
Load Diff
494
build/lib/helpdesk/forms.py
Normal file
494
build/lib/helpdesk/forms.py
Normal file
@ -0,0 +1,494 @@
|
||||
"""
|
||||
django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
forms.py - Definitions of newforms-based forms for creating and maintaining
|
||||
tickets.
|
||||
"""
|
||||
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.six import StringIO
|
||||
from django import forms
|
||||
from django.forms import widgets
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
|
||||
from helpdesk.lib import send_templated_mail, safe_template_context, process_attachments
|
||||
from helpdesk.models import (Ticket, Queue, FollowUp, Attachment, IgnoreEmail, TicketCC,
|
||||
CustomField, TicketCustomFieldValue, TicketDependency)
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
CUSTOMFIELD_TO_FIELD_DICT = {
|
||||
# Store the immediate equivalences here
|
||||
'boolean': forms.BooleanField,
|
||||
'date': forms.DateField,
|
||||
'time': forms.TimeField,
|
||||
'datetime': forms.DateTimeField,
|
||||
'email': forms.EmailField,
|
||||
'url': forms.URLField,
|
||||
'ipaddress': forms.GenericIPAddressField,
|
||||
'slug': forms.SlugField,
|
||||
}
|
||||
|
||||
|
||||
class CustomFieldMixin(object):
|
||||
"""
|
||||
Mixin that provides a method to turn CustomFields into an actual field
|
||||
"""
|
||||
|
||||
def customfield_to_field(self, field, instanceargs):
|
||||
# if-elif branches start with special cases
|
||||
if field.data_type == 'varchar':
|
||||
fieldclass = forms.CharField
|
||||
instanceargs['max_length'] = field.max_length
|
||||
elif field.data_type == 'text':
|
||||
fieldclass = forms.CharField
|
||||
instanceargs['widget'] = forms.Textarea
|
||||
instanceargs['max_length'] = field.max_length
|
||||
elif field.data_type == 'integer':
|
||||
fieldclass = forms.IntegerField
|
||||
elif field.data_type == 'decimal':
|
||||
fieldclass = forms.DecimalField
|
||||
instanceargs['decimal_places'] = field.decimal_places
|
||||
instanceargs['max_digits'] = field.max_length
|
||||
elif field.data_type == 'list':
|
||||
fieldclass = forms.ChoiceField
|
||||
choices = field.choices_as_array
|
||||
if field.empty_selection_list:
|
||||
choices.insert(0, ('', '---------'))
|
||||
instanceargs['choices'] = choices
|
||||
else:
|
||||
# Try to use the immediate equivalences dictionary
|
||||
try:
|
||||
fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type]
|
||||
except KeyError:
|
||||
# The data_type was not found anywhere
|
||||
raise NameError("Unrecognized data_type %s" % field.data_type)
|
||||
|
||||
self.fields['custom_%s' % field.name] = fieldclass(**instanceargs)
|
||||
|
||||
|
||||
class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
exclude = ('created', 'modified', 'status', 'on_hold', 'resolution', 'last_escalation', 'assigned_to')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Add any custom fields that are defined to the form
|
||||
"""
|
||||
super(EditTicketForm, self).__init__(*args, **kwargs)
|
||||
|
||||
for field in CustomField.objects.all():
|
||||
try:
|
||||
current_value = TicketCustomFieldValue.objects.get(ticket=self.instance, field=field)
|
||||
initial_value = current_value.value
|
||||
except TicketCustomFieldValue.DoesNotExist:
|
||||
initial_value = None
|
||||
instanceargs = {
|
||||
'label': field.label,
|
||||
'help_text': field.help_text,
|
||||
'required': field.required,
|
||||
'initial': initial_value,
|
||||
}
|
||||
|
||||
self.customfield_to_field(field, instanceargs)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
for field, value in self.cleaned_data.items():
|
||||
if field.startswith('custom_'):
|
||||
field_name = field.replace('custom_', '', 1)
|
||||
customfield = CustomField.objects.get(name=field_name)
|
||||
try:
|
||||
cfv = TicketCustomFieldValue.objects.get(ticket=self.instance, field=customfield)
|
||||
except ObjectDoesNotExist:
|
||||
cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield)
|
||||
cfv.value = value
|
||||
cfv.save()
|
||||
|
||||
return super(EditTicketForm, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class EditFollowUpForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = FollowUp
|
||||
exclude = ('date', 'user',)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Filter not openned tickets here."""
|
||||
super(EditFollowUpForm, self).__init__(*args, **kwargs)
|
||||
self.fields["ticket"].queryset = Ticket.objects.filter(status__in=(Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS))
|
||||
|
||||
|
||||
class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
"""
|
||||
Contain all the common code and fields between "TicketForm" and
|
||||
"PublicTicketForm". This Form is not intended to be used directly.
|
||||
"""
|
||||
queue = forms.ChoiceField(
|
||||
widget=forms.Select(attrs={'class': 'form-control'}),
|
||||
label=_('Queue'),
|
||||
required=True,
|
||||
choices=()
|
||||
)
|
||||
|
||||
title = forms.CharField(
|
||||
max_length=100,
|
||||
required=True,
|
||||
widget=forms.TextInput(attrs={'class': 'form-control'}),
|
||||
label=_('Summary of the problem'),
|
||||
)
|
||||
|
||||
body = forms.CharField(
|
||||
widget=forms.Textarea(attrs={'class': 'form-control'}),
|
||||
label=_('Description of your issue'),
|
||||
required=True,
|
||||
help_text=_('Please be as descriptive as possible and include all details'),
|
||||
)
|
||||
|
||||
priority = forms.ChoiceField(
|
||||
widget=forms.Select(attrs={'class': 'form-control'}),
|
||||
choices=Ticket.PRIORITY_CHOICES,
|
||||
required=True,
|
||||
initial='3',
|
||||
label=_('Priority'),
|
||||
help_text=_("Please select a priority carefully. If unsure, leave it as '3'."),
|
||||
)
|
||||
|
||||
due_date = forms.DateTimeField(
|
||||
widget=forms.TextInput(attrs={'class': 'form-control'}),
|
||||
required=False,
|
||||
label=_('Due on'),
|
||||
)
|
||||
|
||||
attachment = forms.FileField(
|
||||
required=False,
|
||||
label=_('Attach File'),
|
||||
help_text=_('You can attach a file such as a document or screenshot to this ticket.'),
|
||||
)
|
||||
|
||||
def _add_form_custom_fields(self, staff_only_filter=None):
|
||||
if staff_only_filter is None:
|
||||
queryset = CustomField.objects.all()
|
||||
else:
|
||||
queryset = CustomField.objects.filter(staff_only=staff_only_filter)
|
||||
|
||||
for field in queryset:
|
||||
instanceargs = {
|
||||
'label': field.label,
|
||||
'help_text': field.help_text,
|
||||
'required': field.required,
|
||||
}
|
||||
|
||||
self.customfield_to_field(field, instanceargs)
|
||||
|
||||
def _create_ticket(self):
|
||||
queue = Queue.objects.get(id=int(self.cleaned_data['queue']))
|
||||
|
||||
ticket = Ticket(title=self.cleaned_data['title'],
|
||||
submitter_email=self.cleaned_data['submitter_email'],
|
||||
created=timezone.now(),
|
||||
status=Ticket.OPEN_STATUS,
|
||||
queue=queue,
|
||||
description=self.cleaned_data['body'],
|
||||
priority=self.cleaned_data['priority'],
|
||||
due_date=self.cleaned_data['due_date'],
|
||||
)
|
||||
|
||||
return ticket, queue
|
||||
|
||||
def _create_custom_fields(self, ticket):
|
||||
for field, value in self.cleaned_data.items():
|
||||
if field.startswith('custom_'):
|
||||
field_name = field.replace('custom_', '', 1)
|
||||
custom_field = CustomField.objects.get(name=field_name)
|
||||
cfv = TicketCustomFieldValue(ticket=ticket,
|
||||
field=custom_field,
|
||||
value=value)
|
||||
cfv.save()
|
||||
|
||||
def _create_follow_up(self, ticket, title, user=None):
|
||||
followup = FollowUp(ticket=ticket,
|
||||
title=title,
|
||||
date=timezone.now(),
|
||||
public=True,
|
||||
comment=self.cleaned_data['body'],
|
||||
)
|
||||
if user:
|
||||
followup.user = user
|
||||
return followup
|
||||
|
||||
def _attach_files_to_follow_up(self, followup):
|
||||
files = self.cleaned_data['attachment']
|
||||
if files:
|
||||
files = process_attachments(followup, [files])
|
||||
return files
|
||||
|
||||
@staticmethod
|
||||
def _send_messages(ticket, queue, followup, files, user=None):
|
||||
context = safe_template_context(ticket)
|
||||
context['comment'] = followup.comment
|
||||
|
||||
messages_sent_to = []
|
||||
|
||||
if ticket.submitter_email:
|
||||
send_templated_mail(
|
||||
'newticket_submitter',
|
||||
context,
|
||||
recipients=ticket.submitter_email,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
files=files,
|
||||
)
|
||||
messages_sent_to.append(ticket.submitter_email)
|
||||
|
||||
if ticket.assigned_to and \
|
||||
ticket.assigned_to != user and \
|
||||
ticket.assigned_to.usersettings_helpdesk.settings.get('email_on_ticket_assign', False) and \
|
||||
ticket.assigned_to.email and \
|
||||
ticket.assigned_to.email not in messages_sent_to:
|
||||
send_templated_mail(
|
||||
'assigned_owner',
|
||||
context,
|
||||
recipients=ticket.assigned_to.email,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
files=files,
|
||||
)
|
||||
messages_sent_to.append(ticket.assigned_to.email)
|
||||
|
||||
if queue.new_ticket_cc and queue.new_ticket_cc not in messages_sent_to:
|
||||
send_templated_mail(
|
||||
'newticket_cc',
|
||||
context,
|
||||
recipients=queue.new_ticket_cc,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
files=files,
|
||||
)
|
||||
messages_sent_to.append(queue.new_ticket_cc)
|
||||
|
||||
if queue.updated_ticket_cc and \
|
||||
queue.updated_ticket_cc != queue.new_ticket_cc and \
|
||||
queue.updated_ticket_cc not in messages_sent_to:
|
||||
send_templated_mail(
|
||||
'newticket_cc',
|
||||
context,
|
||||
recipients=queue.updated_ticket_cc,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
files=files,
|
||||
)
|
||||
|
||||
|
||||
class TicketForm(AbstractTicketForm):
|
||||
"""
|
||||
Ticket Form creation for registered users.
|
||||
"""
|
||||
submitter_email = forms.EmailField(
|
||||
required=False,
|
||||
label=_('Submitter E-Mail Address'),
|
||||
widget=forms.TextInput(attrs={'class': 'form-control'}),
|
||||
help_text=_('This e-mail address will receive copies of all public '
|
||||
'updates to this ticket.'),
|
||||
)
|
||||
|
||||
assigned_to = forms.ChoiceField(
|
||||
widget=forms.Select(attrs={'class': 'form-control'}),
|
||||
choices=(),
|
||||
required=False,
|
||||
label=_('Case owner'),
|
||||
help_text=_('If you select an owner other than yourself, they\'ll be '
|
||||
'e-mailed details of this ticket immediately.'),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Add any custom fields that are defined to the form.
|
||||
"""
|
||||
super(TicketForm, self).__init__(*args, **kwargs)
|
||||
self._add_form_custom_fields()
|
||||
|
||||
def save(self, user=None):
|
||||
"""
|
||||
Writes and returns a Ticket() object
|
||||
"""
|
||||
|
||||
ticket, queue = self._create_ticket()
|
||||
if self.cleaned_data['assigned_to']:
|
||||
try:
|
||||
u = User.objects.get(id=self.cleaned_data['assigned_to'])
|
||||
ticket.assigned_to = u
|
||||
except User.DoesNotExist:
|
||||
ticket.assigned_to = None
|
||||
ticket.save()
|
||||
|
||||
self._create_custom_fields(ticket)
|
||||
|
||||
if self.cleaned_data['assigned_to']:
|
||||
title = _('Ticket Opened & Assigned to %(name)s') % {
|
||||
'name': ticket.get_assigned_to or _("<invalid user>")
|
||||
}
|
||||
else:
|
||||
title = _('Ticket Opened')
|
||||
followup = self._create_follow_up(ticket, title=title, user=user)
|
||||
followup.save()
|
||||
|
||||
files = self._attach_files_to_follow_up(followup)
|
||||
self._send_messages(ticket=ticket,
|
||||
queue=queue,
|
||||
followup=followup,
|
||||
files=files,
|
||||
user=user)
|
||||
return ticket
|
||||
|
||||
|
||||
class PublicTicketForm(AbstractTicketForm):
|
||||
"""
|
||||
Ticket Form creation for all users (public-facing).
|
||||
"""
|
||||
submitter_email = forms.EmailField(
|
||||
widget=forms.TextInput(attrs={'class': 'form-control'}),
|
||||
required=True,
|
||||
label=_('Your E-Mail Address'),
|
||||
help_text=_('We will e-mail you when your ticket is updated.'),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Add any (non-staff) custom fields that are defined to the form
|
||||
"""
|
||||
super(PublicTicketForm, self).__init__(*args, **kwargs)
|
||||
|
||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE'):
|
||||
self.fields['queue'].widget = forms.HiddenInput()
|
||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'):
|
||||
self.fields['priority'].widget = forms.HiddenInput()
|
||||
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_DUE_DATE'):
|
||||
self.fields['due_date'].widget = forms.HiddenInput()
|
||||
|
||||
self._add_form_custom_fields(False)
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Writes and returns a Ticket() object
|
||||
"""
|
||||
ticket, queue = self._create_ticket()
|
||||
if queue.default_owner and not ticket.assigned_to:
|
||||
ticket.assigned_to = queue.default_owner
|
||||
ticket.save()
|
||||
|
||||
self._create_custom_fields(ticket)
|
||||
|
||||
followup = self._create_follow_up(ticket, title=_('Ticket Opened Via Web'))
|
||||
followup.save()
|
||||
|
||||
files = self._attach_files_to_follow_up(followup)
|
||||
self._send_messages(ticket=ticket,
|
||||
queue=queue,
|
||||
followup=followup,
|
||||
files=files)
|
||||
return ticket
|
||||
|
||||
|
||||
class UserSettingsForm(forms.Form):
|
||||
login_view_ticketlist = forms.BooleanField(
|
||||
label=_('Show Ticket List on Login?'),
|
||||
help_text=_('Display the ticket list upon login? Otherwise, the dashboard is shown.'),
|
||||
required=False,
|
||||
)
|
||||
|
||||
email_on_ticket_change = forms.BooleanField(
|
||||
label=_('E-mail me on ticket change?'),
|
||||
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?'),
|
||||
required=False,
|
||||
)
|
||||
|
||||
email_on_ticket_assign = forms.BooleanField(
|
||||
label=_('E-mail me when assigned a ticket?'),
|
||||
help_text=_('If you are assigned a ticket via the web, do you want to receive an e-mail?'),
|
||||
required=False,
|
||||
)
|
||||
|
||||
tickets_per_page = forms.ChoiceField(
|
||||
label=_('Number of tickets to show per page'),
|
||||
help_text=_('How many tickets do you want to see on the Ticket List page?'),
|
||||
required=False,
|
||||
choices=((10, '10'), (25, '25'), (50, '50'), (100, '100')),
|
||||
)
|
||||
|
||||
use_email_as_submitter = forms.BooleanField(
|
||||
label=_('Use my e-mail address when submitting tickets?'),
|
||||
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.'),
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
class EmailIgnoreForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = IgnoreEmail
|
||||
exclude = []
|
||||
|
||||
|
||||
class TicketCCForm(forms.ModelForm):
|
||||
''' Adds either an email address or helpdesk user as a CC on a Ticket. Used for processing POST requests. '''
|
||||
|
||||
class Meta:
|
||||
model = TicketCC
|
||||
exclude = ('ticket',)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TicketCCForm, self).__init__(*args, **kwargs)
|
||||
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
|
||||
users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
|
||||
else:
|
||||
users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD)
|
||||
self.fields['user'].queryset = users
|
||||
|
||||
|
||||
class TicketCCUserForm(forms.ModelForm):
|
||||
''' Adds a helpdesk user as a CC on a Ticket '''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TicketCCUserForm, self).__init__(*args, **kwargs)
|
||||
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
|
||||
users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
|
||||
else:
|
||||
users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD)
|
||||
self.fields['user'].queryset = users
|
||||
|
||||
class Meta:
|
||||
model = TicketCC
|
||||
exclude = ('ticket', 'email',)
|
||||
|
||||
|
||||
class TicketCCEmailForm(forms.ModelForm):
|
||||
''' Adds an email address as a CC on a Ticket '''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TicketCCEmailForm, self).__init__(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
model = TicketCC
|
||||
exclude = ('ticket', 'user',)
|
||||
|
||||
|
||||
class TicketDependencyForm(forms.ModelForm):
|
||||
''' Adds a different ticket as a dependency for this Ticket '''
|
||||
|
||||
class Meta:
|
||||
model = TicketDependency
|
||||
exclude = ('ticket',)
|
334
build/lib/helpdesk/lib.py
Normal file
334
build/lib/helpdesk/lib.py
Normal file
@ -0,0 +1,334 @@
|
||||
"""
|
||||
django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
lib.py - Common functions (eg multipart e-mail)
|
||||
"""
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
from smtplib import SMTPException
|
||||
|
||||
try:
|
||||
# Python 2 support
|
||||
from base64 import urlsafe_b64encode as b64encode
|
||||
except ImportError:
|
||||
# Python 3 support
|
||||
from base64 import encodebytes as b64encode
|
||||
try:
|
||||
# Python 2 support
|
||||
from base64 import urlsafe_b64decode as b64decode
|
||||
except ImportError:
|
||||
# Python 3 support
|
||||
from base64 import decodebytes as b64decode
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.utils import six
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from helpdesk.models import Attachment, EmailTemplate
|
||||
|
||||
logger = logging.getLogger('helpdesk')
|
||||
|
||||
|
||||
def send_templated_mail(template_name,
|
||||
context,
|
||||
recipients,
|
||||
sender=None,
|
||||
bcc=None,
|
||||
fail_silently=False,
|
||||
files=None):
|
||||
"""
|
||||
send_templated_mail() is a wrapper around Django's e-mail routines that
|
||||
allows us to easily send multipart (text/plain & text/html) e-mails using
|
||||
templates that are stored in the database. This lets the admin provide
|
||||
both a text and a HTML template for each message.
|
||||
|
||||
template_name is the slug of the template to use for this message (see
|
||||
models.EmailTemplate)
|
||||
|
||||
context is a dictionary to be used when rendering the template
|
||||
|
||||
recipients can be either a string, eg 'a@b.com', or a list of strings.
|
||||
|
||||
sender should contain a string, eg 'My Site <me@z.com>'. If you leave it
|
||||
blank, it'll use settings.DEFAULT_FROM_EMAIL as a fallback.
|
||||
|
||||
bcc is an optional list of addresses that will receive this message as a
|
||||
blind carbon copy.
|
||||
|
||||
fail_silently is passed to Django's mail routine. Set to 'True' to ignore
|
||||
any errors at send time.
|
||||
|
||||
files can be a list of tuples. Each tuple should be a filename to attach,
|
||||
along with the File objects to be read. files can be blank.
|
||||
|
||||
"""
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template import engines
|
||||
from_string = engines['django'].from_string
|
||||
|
||||
from helpdesk.models import EmailTemplate
|
||||
from helpdesk.settings import HELPDESK_EMAIL_SUBJECT_TEMPLATE, \
|
||||
HELPDESK_EMAIL_FALLBACK_LOCALE
|
||||
|
||||
locale = context['queue'].get('locale') or HELPDESK_EMAIL_FALLBACK_LOCALE
|
||||
|
||||
try:
|
||||
t = EmailTemplate.objects.get(template_name__iexact=template_name, locale=locale)
|
||||
except EmailTemplate.DoesNotExist:
|
||||
try:
|
||||
t = EmailTemplate.objects.get(template_name__iexact=template_name, locale__isnull=True)
|
||||
except EmailTemplate.DoesNotExist:
|
||||
logger.warning('template "%s" does not exist, no mail sent', template_name)
|
||||
return # just ignore if template doesn't exist
|
||||
|
||||
subject_part = from_string(
|
||||
HELPDESK_EMAIL_SUBJECT_TEMPLATE % {
|
||||
"subject": t.subject
|
||||
}).render(context).replace('\n', '').replace('\r', '')
|
||||
|
||||
footer_file = os.path.join('helpdesk', locale, 'email_text_footer.txt')
|
||||
|
||||
text_part = from_string(
|
||||
"%s{%% include '%s' %%}" % (t.plain_text, footer_file)
|
||||
).render(context)
|
||||
|
||||
email_html_base_file = os.path.join('helpdesk', locale, 'email_html_base.html')
|
||||
# keep new lines in html emails
|
||||
if 'comment' in context:
|
||||
context['comment'] = mark_safe(context['comment'].replace('\r\n', '<br>'))
|
||||
|
||||
html_part = from_string(
|
||||
"{%% extends '%s' %%}{%% block title %%}"
|
||||
"%s"
|
||||
"{%% endblock %%}{%% block content %%}%s{%% endblock %%}" %
|
||||
(email_html_base_file, t.heading, t.html)
|
||||
).render(context)
|
||||
|
||||
if isinstance(recipients, str):
|
||||
if recipients.find(','):
|
||||
recipients = recipients.split(',')
|
||||
elif type(recipients) != list:
|
||||
recipients = [recipients]
|
||||
|
||||
msg = EmailMultiAlternatives(subject_part, text_part,
|
||||
sender or settings.DEFAULT_FROM_EMAIL,
|
||||
recipients, bcc=bcc)
|
||||
msg.attach_alternative(html_part, "text/html")
|
||||
|
||||
if files:
|
||||
for filename, filefield in files:
|
||||
mime = mimetypes.guess_type(filename)
|
||||
if mime[0] is not None and mime[0] == "text/plain":
|
||||
with open(filefield.path, 'r') as attachedfile:
|
||||
content = attachedfile.read()
|
||||
msg.attach(filename, content)
|
||||
else:
|
||||
if six.PY3:
|
||||
msg.attach_file(filefield.path)
|
||||
else:
|
||||
with open(filefield.path, 'rb') as attachedfile:
|
||||
content = attachedfile.read()
|
||||
msg.attach(filename, content)
|
||||
|
||||
logger.debug('Sending email to: {!r}'.format(recipients))
|
||||
|
||||
try:
|
||||
return msg.send()
|
||||
except SMTPException as e:
|
||||
logger.exception('SMTPException raised while sending email to {}'.format(recipients))
|
||||
if not fail_silently:
|
||||
raise e
|
||||
return 0
|
||||
|
||||
|
||||
def query_to_dict(results, descriptions):
|
||||
"""
|
||||
Replacement method for cursor.dictfetchall() as that method no longer
|
||||
exists in psycopg2, and I'm guessing in other backends too.
|
||||
|
||||
Converts the results of a raw SQL query into a list of dictionaries, suitable
|
||||
for use in templates etc.
|
||||
"""
|
||||
|
||||
output = []
|
||||
for data in results:
|
||||
row = {}
|
||||
i = 0
|
||||
for column in descriptions:
|
||||
row[column[0]] = data[i]
|
||||
i += 1
|
||||
|
||||
output.append(row)
|
||||
return output
|
||||
|
||||
|
||||
def apply_query(queryset, params):
|
||||
"""
|
||||
Apply a dict-based set of filters & parameters to a queryset.
|
||||
|
||||
queryset is a Django queryset, eg MyModel.objects.all() or
|
||||
MyModel.objects.filter(user=request.user)
|
||||
|
||||
params is a dictionary that contains the following:
|
||||
filtering: A dict of Django ORM filters, eg:
|
||||
{'user__id__in': [1, 3, 103], 'title__contains': 'foo'}
|
||||
|
||||
search_string: A freetext search string
|
||||
|
||||
sorting: The name of the column to sort by
|
||||
"""
|
||||
for key in params['filtering'].keys():
|
||||
filter = {key: params['filtering'][key]}
|
||||
queryset = queryset.filter(**filter)
|
||||
|
||||
search = params.get('search_string', None)
|
||||
if search:
|
||||
qset = (
|
||||
Q(title__icontains=search) |
|
||||
Q(description__icontains=search) |
|
||||
Q(resolution__icontains=search) |
|
||||
Q(submitter_email__icontains=search)
|
||||
)
|
||||
|
||||
queryset = queryset.filter(qset)
|
||||
|
||||
sorting = params.get('sorting', None)
|
||||
if sorting:
|
||||
sortreverse = params.get('sortreverse', None)
|
||||
if sortreverse:
|
||||
sorting = "-%s" % sorting
|
||||
queryset = queryset.order_by(sorting)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
def ticket_template_context(ticket):
|
||||
context = {}
|
||||
|
||||
for field in ('title', 'created', 'modified', 'submitter_email',
|
||||
'status', 'get_status_display', 'on_hold', 'description',
|
||||
'resolution', 'priority', 'get_priority_display',
|
||||
'last_escalation', 'ticket', 'ticket_for_url',
|
||||
'get_status', 'ticket_url', 'staff_url', '_get_assigned_to'
|
||||
):
|
||||
attr = getattr(ticket, field, None)
|
||||
if callable(attr):
|
||||
context[field] = '%s' % attr()
|
||||
else:
|
||||
context[field] = attr
|
||||
context['assigned_to'] = context['_get_assigned_to']
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def queue_template_context(queue):
|
||||
context = {}
|
||||
|
||||
for field in ('title', 'slug', 'email_address', 'from_address', 'locale'):
|
||||
attr = getattr(queue, field, None)
|
||||
if callable(attr):
|
||||
context[field] = attr()
|
||||
else:
|
||||
context[field] = attr
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def safe_template_context(ticket):
|
||||
"""
|
||||
Return a dictionary that can be used as a template context to render
|
||||
comments and other details with ticket or queue parameters. Note that
|
||||
we don't just provide the Ticket & Queue objects to the template as
|
||||
they could reveal confidential information. Just imagine these two options:
|
||||
* {{ ticket.queue.email_box_password }}
|
||||
* {{ ticket.assigned_to.password }}
|
||||
|
||||
Ouch!
|
||||
|
||||
The downside to this is that if we make changes to the model, we will also
|
||||
have to update this code. Perhaps we can find a better way in the future.
|
||||
"""
|
||||
|
||||
context = {
|
||||
'queue': queue_template_context(ticket.queue),
|
||||
'ticket': ticket_template_context(ticket),
|
||||
}
|
||||
context['ticket']['queue'] = context['queue']
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def text_is_spam(text, request):
|
||||
# Based on a blog post by 'sciyoshi':
|
||||
# http://sciyoshi.com/blog/2008/aug/27/using-akismet-djangos-new-comments-framework/
|
||||
# This will return 'True' is the given text is deemed to be spam, or
|
||||
# False if it is not spam. If it cannot be checked for some reason, we
|
||||
# assume it isn't spam.
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
try:
|
||||
from helpdesk.akismet import Akismet
|
||||
except ImportError:
|
||||
return False
|
||||
try:
|
||||
site = Site.objects.get_current()
|
||||
except ImproperlyConfigured:
|
||||
site = Site(domain='configure-django-sites.com')
|
||||
|
||||
ak = Akismet(
|
||||
blog_url='http://%s/' % site.domain,
|
||||
agent='django-helpdesk',
|
||||
)
|
||||
|
||||
if hasattr(settings, 'TYPEPAD_ANTISPAM_API_KEY'):
|
||||
ak.setAPIKey(key=settings.TYPEPAD_ANTISPAM_API_KEY)
|
||||
ak.baseurl = 'api.antispam.typepad.com/1.1/'
|
||||
elif hasattr(settings, 'AKISMET_API_KEY'):
|
||||
ak.setAPIKey(key=settings.AKISMET_API_KEY)
|
||||
else:
|
||||
return False
|
||||
|
||||
if ak.verify_key():
|
||||
ak_data = {
|
||||
'user_ip': request.META.get('REMOTE_ADDR', '127.0.0.1'),
|
||||
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
|
||||
'referrer': request.META.get('HTTP_REFERER', ''),
|
||||
'comment_type': 'comment',
|
||||
'comment_author': '',
|
||||
}
|
||||
|
||||
return ak.comment_check(smart_text(text), data=ak_data)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def process_attachments(followup, attached_files):
|
||||
max_email_attachment_size = getattr(settings, 'MAX_EMAIL_ATTACHMENT_SIZE', 512000)
|
||||
attachments = []
|
||||
|
||||
for attached in attached_files:
|
||||
if attached.size:
|
||||
filename = smart_text(attached.name)
|
||||
att = Attachment(
|
||||
followup=followup,
|
||||
file=attached,
|
||||
filename=filename,
|
||||
mime_type=attached.content_type or
|
||||
mimetypes.guess_type(filename, strict=False)[0] or
|
||||
'application/octet-stream',
|
||||
size=attached.size,
|
||||
)
|
||||
att.save()
|
||||
|
||||
if attached.size < max_email_attachment_size:
|
||||
# Only files smaller than 512kb (or as defined in
|
||||
# settings.MAX_EMAIL_ATTACHMENT_SIZE) are sent via email.
|
||||
attachments.append([filename, att.file])
|
||||
|
||||
return attachments
|
BIN
build/lib/helpdesk/locale/ar/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/ar/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2321
build/lib/helpdesk/locale/ar/LC_MESSAGES/django.po
Normal file
2321
build/lib/helpdesk/locale/ar/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/ca/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/ca/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2393
build/lib/helpdesk/locale/ca/LC_MESSAGES/django.po
Normal file
2393
build/lib/helpdesk/locale/ca/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/cs/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/cs/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2393
build/lib/helpdesk/locale/cs/LC_MESSAGES/django.po
Normal file
2393
build/lib/helpdesk/locale/cs/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/de/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/de/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2398
build/lib/helpdesk/locale/de/LC_MESSAGES/django.po
Normal file
2398
build/lib/helpdesk/locale/de/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/el/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/el/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2394
build/lib/helpdesk/locale/el/LC_MESSAGES/django.po
Normal file
2394
build/lib/helpdesk/locale/el/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/en/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/en/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2401
build/lib/helpdesk/locale/en/LC_MESSAGES/django.po
Normal file
2401
build/lib/helpdesk/locale/en/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/es/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/es/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2327
build/lib/helpdesk/locale/es/LC_MESSAGES/django.po
Normal file
2327
build/lib/helpdesk/locale/es/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/es_CO/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/es_CO/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2394
build/lib/helpdesk/locale/es_CO/LC_MESSAGES/django.po
Normal file
2394
build/lib/helpdesk/locale/es_CO/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/es_MX/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/es_MX/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2399
build/lib/helpdesk/locale/es_MX/LC_MESSAGES/django.po
Normal file
2399
build/lib/helpdesk/locale/es_MX/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/fa_IR/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/fa_IR/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2394
build/lib/helpdesk/locale/fa_IR/LC_MESSAGES/django.po
Normal file
2394
build/lib/helpdesk/locale/fa_IR/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/fi/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/fi/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2393
build/lib/helpdesk/locale/fi/LC_MESSAGES/django.po
Normal file
2393
build/lib/helpdesk/locale/fi/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/fr/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/fr/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2401
build/lib/helpdesk/locale/fr/LC_MESSAGES/django.po
Normal file
2401
build/lib/helpdesk/locale/fr/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/he_IL/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/he_IL/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2393
build/lib/helpdesk/locale/he_IL/LC_MESSAGES/django.po
Normal file
2393
build/lib/helpdesk/locale/he_IL/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/hr/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/hr/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2394
build/lib/helpdesk/locale/hr/LC_MESSAGES/django.po
Normal file
2394
build/lib/helpdesk/locale/hr/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/hu/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/hu/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2393
build/lib/helpdesk/locale/hu/LC_MESSAGES/django.po
Normal file
2393
build/lib/helpdesk/locale/hu/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/it/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/it/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2308
build/lib/helpdesk/locale/it/LC_MESSAGES/django.po
Normal file
2308
build/lib/helpdesk/locale/it/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/lv/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/lv/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2393
build/lib/helpdesk/locale/lv/LC_MESSAGES/django.po
Normal file
2393
build/lib/helpdesk/locale/lv/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/nb_NO/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/nb_NO/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2394
build/lib/helpdesk/locale/nb_NO/LC_MESSAGES/django.po
Normal file
2394
build/lib/helpdesk/locale/nb_NO/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/pl/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/pl/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2315
build/lib/helpdesk/locale/pl/LC_MESSAGES/django.po
Normal file
2315
build/lib/helpdesk/locale/pl/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/pt_BR/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/pt_BR/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2312
build/lib/helpdesk/locale/pt_BR/LC_MESSAGES/django.po
Normal file
2312
build/lib/helpdesk/locale/pt_BR/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/pt_PT/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/pt_PT/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2393
build/lib/helpdesk/locale/pt_PT/LC_MESSAGES/django.po
Normal file
2393
build/lib/helpdesk/locale/pt_PT/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/ru/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/ru/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2879
build/lib/helpdesk/locale/ru/LC_MESSAGES/django.po
Normal file
2879
build/lib/helpdesk/locale/ru/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/sv/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/sv/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2322
build/lib/helpdesk/locale/sv/LC_MESSAGES/django.po
Normal file
2322
build/lib/helpdesk/locale/sv/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/th/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/th/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2394
build/lib/helpdesk/locale/th/LC_MESSAGES/django.po
Normal file
2394
build/lib/helpdesk/locale/th/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/tr_TR/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/tr_TR/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2393
build/lib/helpdesk/locale/tr_TR/LC_MESSAGES/django.po
Normal file
2393
build/lib/helpdesk/locale/tr_TR/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/zh_CN/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/zh_CN/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2394
build/lib/helpdesk/locale/zh_CN/LC_MESSAGES/django.po
Normal file
2394
build/lib/helpdesk/locale/zh_CN/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/lib/helpdesk/locale/zh_Hans/LC_MESSAGES/django.mo
Normal file
BIN
build/lib/helpdesk/locale/zh_Hans/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2394
build/lib/helpdesk/locale/zh_Hans/LC_MESSAGES/django.po
Normal file
2394
build/lib/helpdesk/locale/zh_Hans/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
0
build/lib/helpdesk/management/__init__.py
Normal file
0
build/lib/helpdesk/management/__init__.py
Normal file
0
build/lib/helpdesk/management/commands/__init__.py
Normal file
0
build/lib/helpdesk/management/commands/__init__.py
Normal file
@ -0,0 +1,156 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
scripts/create_escalation_exclusion.py - Easy way to routinely add particular
|
||||
days to the list of days on which no
|
||||
escalation should take place.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
from datetime import timedelta, date
|
||||
import getopt
|
||||
from optparse import make_option
|
||||
import sys
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from helpdesk.models import EscalationExclusion, Queue
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
def __init__(self):
|
||||
BaseCommand.__init__(self)
|
||||
|
||||
self.option_list += (
|
||||
make_option(
|
||||
'--days', '-d',
|
||||
help='Days of week (monday, tuesday, etc)'),
|
||||
make_option(
|
||||
'--occurrences', '-o',
|
||||
type='int',
|
||||
default=1,
|
||||
help='Occurrences: How many weeks ahead to exclude this day'),
|
||||
make_option(
|
||||
'--queues', '-q',
|
||||
help='Queues to include (default: all). Use queue slugs'),
|
||||
make_option(
|
||||
'--escalate-verbosely', '-x',
|
||||
action='store_true',
|
||||
default=False,
|
||||
dest='escalate-verbosely',
|
||||
help='Display a list of dates excluded'),
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
days = options['days']
|
||||
# optparse should already handle the `or 1`
|
||||
occurrences = options['occurrences'] or 1
|
||||
verbose = False
|
||||
queue_slugs = options['queues']
|
||||
queues = []
|
||||
|
||||
if options['escalate-verbosely']:
|
||||
verbose = True
|
||||
|
||||
if not (days and occurrences):
|
||||
raise CommandError('One or more occurrences must be specified.')
|
||||
|
||||
if queue_slugs is not None:
|
||||
queue_set = queue_slugs.split(',')
|
||||
for queue in queue_set:
|
||||
try:
|
||||
q = Queue.objects.get(slug__exact=queue)
|
||||
except Queue.DoesNotExist:
|
||||
raise CommandError("Queue %s does not exist." % queue)
|
||||
queues.append(q)
|
||||
|
||||
create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues)
|
||||
|
||||
|
||||
day_names = {
|
||||
'monday': 0,
|
||||
'tuesday': 1,
|
||||
'wednesday': 2,
|
||||
'thursday': 3,
|
||||
'friday': 4,
|
||||
'saturday': 5,
|
||||
'sunday': 6,
|
||||
}
|
||||
|
||||
|
||||
def create_exclusions(days, occurrences, verbose, queues):
|
||||
days = days.split(',')
|
||||
for day in days:
|
||||
day_name = day
|
||||
day = day_names[day]
|
||||
workdate = date.today()
|
||||
i = 0
|
||||
while i < occurrences:
|
||||
if day == workdate.weekday():
|
||||
if EscalationExclusion.objects.filter(date=workdate).count() == 0:
|
||||
esc = EscalationExclusion(name='Auto Exclusion for %s' % day_name, date=workdate)
|
||||
esc.save()
|
||||
|
||||
if verbose:
|
||||
print("Created exclusion for %s %s" % (day_name, workdate))
|
||||
|
||||
for q in queues:
|
||||
esc.queues.add(q)
|
||||
if verbose:
|
||||
print(" - for queue %s" % q)
|
||||
|
||||
i += 1
|
||||
workdate += timedelta(days=1)
|
||||
|
||||
|
||||
def usage():
|
||||
print("Options:")
|
||||
print(" --days, -d: Days of week (monday, tuesday, etc)")
|
||||
print(" --occurrences, -o: Occurrences: How many weeks ahead to exclude this day")
|
||||
print(" --queues, -q: Queues to include (default: all). Use queue slugs")
|
||||
print(" --verbose, -v: Display a list of dates excluded")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# This script can be run from the command-line or via Django's manage.py.
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', ['days=', 'occurrences=', 'verbose', 'queues='])
|
||||
except getopt.GetoptError:
|
||||
usage()
|
||||
sys.exit(2)
|
||||
|
||||
days = None
|
||||
occurrences = 1
|
||||
verbose = False
|
||||
queue_slugs = None
|
||||
queues = []
|
||||
|
||||
for o, a in opts:
|
||||
if o in ('-x', '--escalate-verbosely'):
|
||||
verbose = True
|
||||
if o in ('-d', '--days'):
|
||||
days = a
|
||||
if o in ('-q', '--queues'):
|
||||
queue_slugs = a
|
||||
if o in ('-o', '--occurrences'):
|
||||
occurrences = int(a) or 1
|
||||
|
||||
if not (days and occurrences):
|
||||
usage()
|
||||
sys.exit(2)
|
||||
|
||||
if queue_slugs is not None:
|
||||
queue_set = queue_slugs.split(',')
|
||||
for queue in queue_set:
|
||||
try:
|
||||
q = Queue.objects.get(slug__exact=queue)
|
||||
except Queue.DoesNotExist:
|
||||
print("Queue %s does not exist." % queue)
|
||||
sys.exit(2)
|
||||
queues.append(q)
|
||||
|
||||
create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues)
|
@ -0,0 +1,74 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
scripts/create_queue_permissions.py -
|
||||
Create automatically permissions for all Queues.
|
||||
|
||||
This is rarely needed. However, one use case is the scenario where the
|
||||
slugs of the Queues have been changed, and thus the Permission should be
|
||||
recreated according to the new slugs.
|
||||
|
||||
No cleanup of permissions is performed.
|
||||
|
||||
It should be safe to call this script multiple times or with partial
|
||||
existing permissions.
|
||||
"""
|
||||
|
||||
from optparse import make_option
|
||||
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db.utils import IntegrityError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from helpdesk.models import Queue
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
def __init__(self):
|
||||
BaseCommand.__init__(self)
|
||||
|
||||
self.option_list += (
|
||||
make_option(
|
||||
'--queues', '-q',
|
||||
help='Queues to include (default: all). Use queue slugs'),
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
queue_slugs = options['queues']
|
||||
queues = []
|
||||
|
||||
if queue_slugs is not None:
|
||||
queue_set = queue_slugs.split(',')
|
||||
for queue in queue_set:
|
||||
try:
|
||||
q = Queue.objects.get(slug__exact=queue)
|
||||
except Queue.DoesNotExist:
|
||||
raise CommandError("Queue %s does not exist." % queue)
|
||||
queues.append(q)
|
||||
else:
|
||||
queues = list(Queue.objects.all())
|
||||
|
||||
# Create permissions for the queues, which may be all or not
|
||||
for q in queues:
|
||||
self.stdout.write("Preparing Queue %s [%s]" % (q.title, q.slug))
|
||||
|
||||
if q.permission_name:
|
||||
self.stdout.write(" .. already has `permission_name=%s`" % q.permission_name)
|
||||
basename = q.permission_name[9:]
|
||||
else:
|
||||
basename = q.generate_permission_name()
|
||||
self.stdout.write(" .. generated `permission_name=%s`" % q.permission_name)
|
||||
q.save()
|
||||
|
||||
self.stdout.write(" .. checking permission codename `%s`" % basename)
|
||||
|
||||
try:
|
||||
Permission.objects.create(
|
||||
name=_("Permission for queue: ") + q.title,
|
||||
content_type=ContentType.objects.get(model="queue"),
|
||||
codename=basename,
|
||||
)
|
||||
except IntegrityError:
|
||||
self.stdout.write(" .. permission already existed, skipping")
|
@ -0,0 +1,33 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
See LICENSE for details.
|
||||
|
||||
create_usersettings.py - Easy way to create helpdesk-specific settings for
|
||||
users who don't yet have them.
|
||||
"""
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from helpdesk.models import UserSettings
|
||||
from helpdesk.settings import DEFAULT_USER_SETTINGS
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""create_usersettings command"""
|
||||
|
||||
help = _('Check for user without django-helpdesk UserSettings '
|
||||
'and create settings if required. Uses '
|
||||
'settings.DEFAULT_USER_SETTINGS which can be overridden to '
|
||||
'suit your situation.')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""handle command line"""
|
||||
for u in User.objects.all():
|
||||
UserSettings.objects.get_or_create(user=u,
|
||||
defaults={'settings': DEFAULT_USER_SETTINGS})
|
196
build/lib/helpdesk/management/commands/escalate_tickets.py
Normal file
196
build/lib/helpdesk/management/commands/escalate_tickets.py
Normal file
@ -0,0 +1,196 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
scripts/escalate_tickets.py - Easy way to escalate tickets based on their age,
|
||||
designed to be run from Cron or similar.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
from datetime import timedelta, date
|
||||
import getopt
|
||||
from optparse import make_option
|
||||
import sys
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
try:
|
||||
from django.utils import timezone
|
||||
except ImportError:
|
||||
from datetime import datetime as timezone
|
||||
|
||||
from helpdesk.models import Queue, Ticket, FollowUp, EscalationExclusion, TicketChange
|
||||
from helpdesk.lib import send_templated_mail, safe_template_context
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
def __init__(self):
|
||||
BaseCommand.__init__(self)
|
||||
|
||||
self.option_list += (
|
||||
make_option(
|
||||
'--queues',
|
||||
help='Queues to include (default: all). Use queue slugs'),
|
||||
make_option(
|
||||
'--verboseescalation',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Display a list of dates excluded'),
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
verbose = False
|
||||
queue_slugs = None
|
||||
queues = []
|
||||
|
||||
if options['verboseescalation']:
|
||||
verbose = True
|
||||
if options['queues']:
|
||||
queue_slugs = options['queues']
|
||||
|
||||
if queue_slugs is not None:
|
||||
queue_set = queue_slugs.split(',')
|
||||
for queue in queue_set:
|
||||
try:
|
||||
Queue.objects.get(slug__exact=queue)
|
||||
except Queue.DoesNotExist:
|
||||
raise CommandError("Queue %s does not exist." % queue)
|
||||
queues.append(queue)
|
||||
|
||||
escalate_tickets(queues=queues, verbose=verbose)
|
||||
|
||||
|
||||
def escalate_tickets(queues, verbose):
|
||||
""" Only include queues with escalation configured """
|
||||
queryset = Queue.objects.filter(escalate_days__isnull=False).exclude(escalate_days=0)
|
||||
if queues:
|
||||
queryset = queryset.filter(slug__in=queues)
|
||||
|
||||
for q in queryset:
|
||||
last = date.today() - timedelta(days=q.escalate_days)
|
||||
today = date.today()
|
||||
workdate = last
|
||||
|
||||
days = 0
|
||||
|
||||
while workdate < today:
|
||||
if EscalationExclusion.objects.filter(date=workdate).count() == 0:
|
||||
days += 1
|
||||
workdate = workdate + timedelta(days=1)
|
||||
|
||||
req_last_escl_date = date.today() - timedelta(days=days)
|
||||
|
||||
if verbose:
|
||||
print("Processing: %s" % q)
|
||||
|
||||
for t in q.ticket_set.filter(
|
||||
Q(status=Ticket.OPEN_STATUS) |
|
||||
Q(status=Ticket.REOPENED_STATUS)
|
||||
).exclude(
|
||||
priority=1
|
||||
).filter(
|
||||
Q(on_hold__isnull=True) |
|
||||
Q(on_hold=False)
|
||||
).filter(
|
||||
Q(last_escalation__lte=req_last_escl_date) |
|
||||
Q(last_escalation__isnull=True, created__lte=req_last_escl_date)
|
||||
):
|
||||
|
||||
t.last_escalation = timezone.now()
|
||||
t.priority -= 1
|
||||
t.save()
|
||||
|
||||
context = safe_template_context(t)
|
||||
|
||||
if t.submitter_email:
|
||||
send_templated_mail(
|
||||
'escalated_submitter',
|
||||
context,
|
||||
recipients=t.submitter_email,
|
||||
sender=t.queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if t.queue.updated_ticket_cc:
|
||||
send_templated_mail(
|
||||
'escalated_cc',
|
||||
context,
|
||||
recipients=t.queue.updated_ticket_cc,
|
||||
sender=t.queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if t.assigned_to:
|
||||
send_templated_mail(
|
||||
'escalated_owner',
|
||||
context,
|
||||
recipients=t.assigned_to.email,
|
||||
sender=t.queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if verbose:
|
||||
print(" - Esclating %s from %s>%s" % (
|
||||
t.ticket,
|
||||
t.priority + 1,
|
||||
t.priority
|
||||
)
|
||||
)
|
||||
|
||||
f = FollowUp(
|
||||
ticket=t,
|
||||
title='Ticket Escalated',
|
||||
date=timezone.now(),
|
||||
public=True,
|
||||
comment=_('Ticket escalated after %s days' % q.escalate_days),
|
||||
)
|
||||
f.save()
|
||||
|
||||
tc = TicketChange(
|
||||
followup=f,
|
||||
field=_('Priority'),
|
||||
old_value=t.priority + 1,
|
||||
new_value=t.priority,
|
||||
)
|
||||
tc.save()
|
||||
|
||||
|
||||
def usage():
|
||||
print("Options:")
|
||||
print(" --queues: Queues to include (default: all). Use queue slugs")
|
||||
print(" --verboseescalation: Display a list of dates excluded")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], ['queues=', 'verboseescalation'])
|
||||
except getopt.GetoptError:
|
||||
usage()
|
||||
sys.exit(2)
|
||||
|
||||
verbose = False
|
||||
queue_slugs = None
|
||||
queues = []
|
||||
|
||||
for o, a in opts:
|
||||
if o == '--verboseescalation':
|
||||
verbose = True
|
||||
if o == '--queues':
|
||||
queue_slugs = a
|
||||
|
||||
if queue_slugs is not None:
|
||||
queue_set = queue_slugs.split(',')
|
||||
for queue in queue_set:
|
||||
try:
|
||||
q = Queue.objects.get(slug__exact=queue)
|
||||
except Queue.DoesNotExist:
|
||||
print("Queue %s does not exist." % queue)
|
||||
sys.exit(2)
|
||||
queues.append(queue)
|
||||
|
||||
escalate_tickets(queues=queues, verbose=verbose)
|
553
build/lib/helpdesk/management/commands/get_email.py
Normal file
553
build/lib/helpdesk/management/commands/get_email.py
Normal file
@ -0,0 +1,553 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
scripts/get_email.py - Designed to be run from cron, this script checks the
|
||||
POP and IMAP boxes, or a local mailbox directory,
|
||||
defined for the queues within a
|
||||
helpdesk, creating tickets from the new messages (or
|
||||
adding to existing tickets if needed)
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import timedelta
|
||||
import base64
|
||||
import binascii
|
||||
import email
|
||||
import imaplib
|
||||
import mimetypes
|
||||
from os import listdir, unlink
|
||||
from os.path import isfile, join
|
||||
import poplib
|
||||
import re
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
from time import ctime
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from email_reply_parser import EmailReplyParser
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils import encoding, six, timezone
|
||||
|
||||
from helpdesk import settings
|
||||
from helpdesk.lib import send_templated_mail, safe_template_context, process_attachments
|
||||
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, IgnoreEmail
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
STRIPPED_SUBJECT_STRINGS = [
|
||||
"Re: ",
|
||||
"Fw: ",
|
||||
"RE: ",
|
||||
"FW: ",
|
||||
"Automatic reply: ",
|
||||
]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
def __init__(self):
|
||||
BaseCommand.__init__(self)
|
||||
|
||||
help = 'Process django-helpdesk queues and process e-mails via POP3/IMAP or ' \
|
||||
'from a local mailbox directory as required, feeding them into the helpdesk.'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--quiet',
|
||||
action='store_true',
|
||||
dest='quiet',
|
||||
default=False,
|
||||
help='Hide details about each queue/message as they are processed',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
quiet = options.get('quiet', False)
|
||||
process_email(quiet=quiet)
|
||||
|
||||
|
||||
def process_email(quiet=False):
|
||||
for q in Queue.objects.filter(
|
||||
email_box_type__isnull=False,
|
||||
allow_email_submission=True):
|
||||
|
||||
logger = logging.getLogger('django.helpdesk.queue.' + q.slug)
|
||||
if not q.logging_type or q.logging_type == 'none':
|
||||
logging.disable(logging.CRITICAL) # disable all messages
|
||||
elif q.logging_type == 'info':
|
||||
logger.setLevel(logging.INFO)
|
||||
elif q.logging_type == 'warn':
|
||||
logger.setLevel(logging.WARN)
|
||||
elif q.logging_type == 'error':
|
||||
logger.setLevel(logging.ERROR)
|
||||
elif q.logging_type == 'crit':
|
||||
logger.setLevel(logging.CRITICAL)
|
||||
elif q.logging_type == 'debug':
|
||||
logger.setLevel(logging.DEBUG)
|
||||
if quiet:
|
||||
logger.propagate = False # do not propagate to root logger that would log to console
|
||||
logdir = q.logging_dir or '/var/log/helpdesk/'
|
||||
handler = logging.FileHandler(join(logdir, q.slug + '_get_email.log'))
|
||||
logger.addHandler(handler)
|
||||
|
||||
if not q.email_box_last_check:
|
||||
q.email_box_last_check = timezone.now() - timedelta(minutes=30)
|
||||
|
||||
queue_time_delta = timedelta(minutes=q.email_box_interval or 0)
|
||||
|
||||
if (q.email_box_last_check + queue_time_delta) < timezone.now():
|
||||
process_queue(q, logger=logger)
|
||||
q.email_box_last_check = timezone.now()
|
||||
q.save()
|
||||
|
||||
|
||||
def process_queue(q, logger):
|
||||
logger.info("***** %s: Begin processing mail for django-helpdesk" % ctime())
|
||||
|
||||
if q.socks_proxy_type and q.socks_proxy_host and q.socks_proxy_port:
|
||||
try:
|
||||
import socks
|
||||
except ImportError:
|
||||
no_socks_msg = "Queue has been configured with proxy settings, " \
|
||||
"but no socks library was installed. Try to " \
|
||||
"install PySocks via PyPI."
|
||||
logger.error(no_socks_msg)
|
||||
raise ImportError(no_socks_msg)
|
||||
|
||||
proxy_type = {
|
||||
'socks4': socks.SOCKS4,
|
||||
'socks5': socks.SOCKS5,
|
||||
}.get(q.socks_proxy_type)
|
||||
|
||||
socks.set_default_proxy(proxy_type=proxy_type,
|
||||
addr=q.socks_proxy_host,
|
||||
port=q.socks_proxy_port)
|
||||
socket.socket = socks.socksocket
|
||||
elif six.PY2:
|
||||
socket.socket = socket._socketobject
|
||||
|
||||
email_box_type = settings.QUEUE_EMAIL_BOX_TYPE or q.email_box_type
|
||||
|
||||
if email_box_type == 'pop3':
|
||||
if q.email_box_ssl or settings.QUEUE_EMAIL_BOX_SSL:
|
||||
if not q.email_box_port:
|
||||
q.email_box_port = 995
|
||||
server = poplib.POP3_SSL(q.email_box_host or
|
||||
settings.QUEUE_EMAIL_BOX_HOST,
|
||||
int(q.email_box_port))
|
||||
else:
|
||||
if not q.email_box_port:
|
||||
q.email_box_port = 110
|
||||
server = poplib.POP3(q.email_box_host or
|
||||
settings.QUEUE_EMAIL_BOX_HOST,
|
||||
int(q.email_box_port))
|
||||
|
||||
logger.info("Attempting POP3 server login")
|
||||
|
||||
server.getwelcome()
|
||||
server.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER)
|
||||
server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD)
|
||||
|
||||
messagesInfo = server.list()[1]
|
||||
logger.info("Received %d messages from POP3 server" % len(messagesInfo))
|
||||
|
||||
for msgRaw in messagesInfo:
|
||||
if six.PY3 and type(msgRaw) is bytes:
|
||||
# in py3, msgRaw may be a bytes object, decode to str
|
||||
try:
|
||||
msg = msgRaw.decode("utf-8")
|
||||
except UnicodeError:
|
||||
# if couldn't decode easily, just leave it raw
|
||||
msg = msgRaw
|
||||
else:
|
||||
# already a str
|
||||
msg = msgRaw
|
||||
msgNum = msg.split(" ")[0]
|
||||
logger.info("Processing message %s" % msgNum)
|
||||
|
||||
if six.PY2:
|
||||
full_message = encoding.force_text("\n".join(server.retr(msgNum)[1]), errors='replace')
|
||||
else:
|
||||
raw_content = server.retr(msgNum)[1]
|
||||
if type(raw_content[0]) is bytes:
|
||||
full_message = "\n".join([elm.decode('utf-8') for elm in raw_content])
|
||||
else:
|
||||
full_message = encoding.force_text("\n".join(raw_content), errors='replace')
|
||||
ticket = ticket_from_message(message=full_message, queue=q, logger=logger)
|
||||
|
||||
if ticket:
|
||||
server.dele(msgNum)
|
||||
logger.info("Successfully processed message %s, deleted from POP3 server" % msgNum)
|
||||
else:
|
||||
logger.warn("Message %s was not successfully processed, and will be left on POP3 server" % msgNum)
|
||||
|
||||
server.quit()
|
||||
|
||||
elif email_box_type == 'imap':
|
||||
if q.email_box_ssl or settings.QUEUE_EMAIL_BOX_SSL:
|
||||
if not q.email_box_port:
|
||||
q.email_box_port = 993
|
||||
server = imaplib.IMAP4_SSL(q.email_box_host or
|
||||
settings.QUEUE_EMAIL_BOX_HOST,
|
||||
int(q.email_box_port))
|
||||
else:
|
||||
if not q.email_box_port:
|
||||
q.email_box_port = 143
|
||||
server = imaplib.IMAP4(q.email_box_host or
|
||||
settings.QUEUE_EMAIL_BOX_HOST,
|
||||
int(q.email_box_port))
|
||||
|
||||
logger.info("Attempting IMAP server login")
|
||||
|
||||
try:
|
||||
server.login(q.email_box_user or
|
||||
settings.QUEUE_EMAIL_BOX_USER,
|
||||
q.email_box_pass or
|
||||
settings.QUEUE_EMAIL_BOX_PASSWORD)
|
||||
server.select(q.email_box_imap_folder)
|
||||
except imaplib.IMAP4.abort:
|
||||
logger.error("IMAP login failed. Check that the server is accessible and that the username and password are correct.")
|
||||
server.logout()
|
||||
sys.exit()
|
||||
except ssl.SSLError:
|
||||
logger.error("IMAP login failed due to SSL error. This is often due to a timeout. Please check your connection and try again.")
|
||||
server.logout()
|
||||
sys.exit()
|
||||
|
||||
try:
|
||||
status, data = server.search(None, 'NOT', 'DELETED')
|
||||
except imaplib.IMAP4.error:
|
||||
logger.error("IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?" % q.email_box_imap_folder)
|
||||
if data:
|
||||
msgnums = data[0].split()
|
||||
logger.info("Received %d messages from IMAP server" % len(msgnums))
|
||||
for num in msgnums:
|
||||
logger.info("Processing message %s" % num)
|
||||
status, data = server.fetch(num, '(RFC822)')
|
||||
full_message = encoding.force_text(data[0][1], errors='replace')
|
||||
try:
|
||||
ticket = ticket_from_message(message=full_message, queue=q, logger=logger)
|
||||
except TypeError:
|
||||
ticket = None # hotfix. Need to work out WHY.
|
||||
if ticket:
|
||||
server.store(num, '+FLAGS', '\\Deleted')
|
||||
logger.info("Successfully processed message %s, deleted from IMAP server" % num)
|
||||
else:
|
||||
logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % num)
|
||||
|
||||
server.expunge()
|
||||
server.close()
|
||||
server.logout()
|
||||
|
||||
elif email_box_type == 'local':
|
||||
mail_dir = q.email_box_local_dir or '/var/lib/mail/helpdesk/'
|
||||
mail = [join(mail_dir, f) for f in listdir(mail_dir) if isfile(join(mail_dir, f))]
|
||||
logger.info("Found %d messages in local mailbox directory" % len(mail))
|
||||
|
||||
logger.info("Found %d messages in local mailbox directory" % len(mail))
|
||||
for i, m in enumerate(mail, 1):
|
||||
logger.info("Processing message %d" % i)
|
||||
with open(m, 'r') as f:
|
||||
full_message = encoding.force_text(f.read(), errors='replace')
|
||||
ticket = ticket_from_message(message=full_message, queue=q, logger=logger)
|
||||
if ticket:
|
||||
logger.info("Successfully processed message %d, ticket/comment created." % i)
|
||||
try:
|
||||
unlink(m) # delete message file if ticket was successful
|
||||
except OSError:
|
||||
logger.error("Unable to delete message %d." % i)
|
||||
else:
|
||||
logger.info("Successfully deleted message %d." % i)
|
||||
else:
|
||||
logger.warn("Message %d was not successfully processed, and will be left in local directory" % i)
|
||||
|
||||
|
||||
def decodeUnknown(charset, string):
|
||||
if six.PY2:
|
||||
if not charset:
|
||||
try:
|
||||
return string.decode('utf-8', 'replace')
|
||||
except UnicodeError:
|
||||
return string.decode('iso8859-1', 'replace')
|
||||
return unicode(string, charset)
|
||||
elif six.PY3:
|
||||
if type(string) is not str:
|
||||
if not charset:
|
||||
try:
|
||||
return str(string, encoding='utf-8', errors='replace')
|
||||
except UnicodeError:
|
||||
return str(string, encoding='iso8859-1', errors='replace')
|
||||
return str(string, encoding=charset, errors='replace')
|
||||
return string
|
||||
|
||||
|
||||
def decode_mail_headers(string):
|
||||
decoded = email.header.decode_header(string) if six.PY3 else email.header.decode_header(string.encode('utf-8'))
|
||||
if six.PY2:
|
||||
return u' '.join([unicode(msg, charset or 'utf-8') for msg, charset in decoded])
|
||||
elif six.PY3:
|
||||
return u' '.join([str(msg, encoding=charset, errors='replace') if charset else str(msg) for msg, charset in decoded])
|
||||
|
||||
|
||||
def ticket_from_message(message, queue, logger):
|
||||
# 'message' must be an RFC822 formatted message.
|
||||
message = email.message_from_string(message) if six.PY3 else email.message_from_string(message.encode('utf-8'))
|
||||
subject = message.get('subject', _('Comment from e-mail'))
|
||||
subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject))
|
||||
for affix in STRIPPED_SUBJECT_STRINGS:
|
||||
subject = subject.replace(affix, "")
|
||||
subject = subject.strip()
|
||||
|
||||
sender = message.get('from', _('Unknown Sender'))
|
||||
sender = decode_mail_headers(decodeUnknown(message.get_charset(), sender))
|
||||
sender_email = email.utils.parseaddr(sender)[1]
|
||||
|
||||
cc = message.get_all('cc', None)
|
||||
if cc:
|
||||
# first, fixup the encoding if necessary
|
||||
cc = [decode_mail_headers(decodeUnknown(message.get_charset(), x)) for x in cc]
|
||||
# get_all checks if multiple CC headers, but individual emails may be comma separated too
|
||||
tempcc = []
|
||||
for hdr in cc:
|
||||
tempcc.extend(hdr.split(','))
|
||||
# use a set to ensure no duplicates
|
||||
cc = set([x.strip() for x in tempcc])
|
||||
|
||||
for ignore in IgnoreEmail.objects.filter(Q(queues=queue) | Q(queues__isnull=True)):
|
||||
if ignore.test(sender_email):
|
||||
if ignore.keep_in_mailbox:
|
||||
# By returning 'False' the message will be kept in the mailbox,
|
||||
# and the 'True' will cause the message to be deleted.
|
||||
return False
|
||||
return True
|
||||
|
||||
matchobj = re.match(r".*\[" + queue.slug + r"-(?P<id>\d+)\]", subject)
|
||||
if matchobj:
|
||||
# This is a reply or forward.
|
||||
ticket = matchobj.group('id')
|
||||
logger.info("Matched tracking ID %s-%s" % (queue.slug, ticket))
|
||||
else:
|
||||
logger.info("No tracking ID matched.")
|
||||
ticket = None
|
||||
|
||||
body = None
|
||||
counter = 0
|
||||
files = []
|
||||
|
||||
for part in message.walk():
|
||||
if part.get_content_maintype() == 'multipart':
|
||||
continue
|
||||
|
||||
name = part.get_param("name")
|
||||
if name:
|
||||
name = email.utils.collapse_rfc2231_value(name)
|
||||
|
||||
if part.get_content_maintype() == 'text' and name is None:
|
||||
if part.get_content_subtype() == 'plain':
|
||||
body = EmailReplyParser.parse_reply(
|
||||
decodeUnknown(part.get_content_charset(), part.get_payload(decode=True))
|
||||
)
|
||||
# workaround to get unicode text out rather than escaped text
|
||||
try:
|
||||
body = body.encode('ascii').decode('unicode_escape')
|
||||
except UnicodeEncodeError:
|
||||
body.encode('utf-8')
|
||||
logger.debug("Discovered plain text MIME part")
|
||||
else:
|
||||
files.append(
|
||||
SimpleUploadedFile(_("email_html_body.html"), encoding.smart_bytes(part.get_payload()), 'text/html')
|
||||
)
|
||||
logger.debug("Discovered HTML MIME part")
|
||||
else:
|
||||
if not name:
|
||||
ext = mimetypes.guess_extension(part.get_content_type())
|
||||
name = "part-%i%s" % (counter, ext)
|
||||
payload = part.get_payload()
|
||||
if isinstance(payload, list):
|
||||
payload = payload.pop().as_string()
|
||||
payloadToWrite = payload
|
||||
# check version of python to ensure use of only the correct error type
|
||||
if six.PY2:
|
||||
non_b64_err = binascii.Error
|
||||
else:
|
||||
non_b64_err = TypeError
|
||||
try:
|
||||
logger.debug("Try to base64 decode the attachment payload")
|
||||
if six.PY2:
|
||||
payloadToWrite = base64.decodestring(payload)
|
||||
else:
|
||||
payloadToWrite = base64.decodebytes(payload)
|
||||
except non_b64_err:
|
||||
logger.debug("Payload was not base64 encoded, using raw bytes")
|
||||
payloadToWrite = payload
|
||||
files.append(SimpleUploadedFile(name, part.get_payload(decode=True), mimetypes.guess_type(name)[0]))
|
||||
logger.debug("Found MIME attachment %s" % name)
|
||||
|
||||
counter += 1
|
||||
|
||||
if not body:
|
||||
mail = BeautifulSoup(part.get_payload(), "lxml")
|
||||
if ">" in mail.text:
|
||||
body = mail.find('body')
|
||||
body = body.text
|
||||
body = body.encode('ascii', errors='ignore')
|
||||
else:
|
||||
body = mail.text
|
||||
|
||||
if ticket:
|
||||
try:
|
||||
t = Ticket.objects.get(id=ticket)
|
||||
except Ticket.DoesNotExist:
|
||||
logger.info("Tracking ID %s-%s not associated with existing ticket. Creating new ticket." % (queue.slug, ticket))
|
||||
ticket = None
|
||||
else:
|
||||
logger.info("Found existing ticket with Tracking ID %s-%s" % (t.queue.slug, t.id))
|
||||
if t.status == Ticket.CLOSED_STATUS:
|
||||
t.status = Ticket.REOPENED_STATUS
|
||||
t.save()
|
||||
new = False
|
||||
|
||||
smtp_priority = message.get('priority', '')
|
||||
smtp_importance = message.get('importance', '')
|
||||
high_priority_types = {'high', 'important', '1', 'urgent'}
|
||||
priority = 2 if high_priority_types & {smtp_priority, smtp_importance} else 3
|
||||
|
||||
if ticket is None:
|
||||
if settings.QUEUE_EMAIL_BOX_UPDATE_ONLY:
|
||||
return None
|
||||
new = True
|
||||
t = Ticket.objects.create(
|
||||
title=subject,
|
||||
queue=queue,
|
||||
submitter_email=sender_email,
|
||||
created=timezone.now(),
|
||||
description=body,
|
||||
priority=priority,
|
||||
)
|
||||
logger.debug("Created new ticket %s-%s" % (t.queue.slug, t.id))
|
||||
|
||||
if cc:
|
||||
# get list of currently CC'd emails
|
||||
current_cc = TicketCC.objects.filter(ticket=ticket)
|
||||
current_cc_emails = [x.email for x in current_cc if x.email]
|
||||
# get emails of any Users CC'd to email, if defined
|
||||
# (some Users may not have an associated email, e.g, when using LDAP)
|
||||
current_cc_users = [x.user.email for x in current_cc if x.user and x.user.email]
|
||||
# ensure submitter, assigned user, queue email not added
|
||||
other_emails = [queue.email_address]
|
||||
if t.submitter_email:
|
||||
other_emails.append(t.submitter_email)
|
||||
if t.assigned_to:
|
||||
other_emails.append(t.assigned_to.email)
|
||||
current_cc = set(current_cc_emails + current_cc_users + other_emails)
|
||||
# first, add any User not previously CC'd (as identified by User's email)
|
||||
all_users = User.objects.all()
|
||||
all_user_emails = set([x.email for x in all_users])
|
||||
users_not_currently_ccd = all_user_emails.difference(set(current_cc))
|
||||
users_to_cc = cc.intersection(users_not_currently_ccd)
|
||||
for user in users_to_cc:
|
||||
tcc = TicketCC.objects.create(
|
||||
ticket=t,
|
||||
user=User.objects.get(email=user),
|
||||
can_view=True,
|
||||
can_update=False
|
||||
)
|
||||
tcc.save()
|
||||
# then add remaining emails alphabetically, makes testing easy
|
||||
new_cc = cc.difference(current_cc).difference(all_user_emails)
|
||||
new_cc = sorted(list(new_cc))
|
||||
for ccemail in new_cc:
|
||||
tcc = TicketCC.objects.create(
|
||||
ticket=t,
|
||||
email=ccemail.replace('\n', ' ').replace('\r', ' '),
|
||||
can_view=True,
|
||||
can_update=False
|
||||
)
|
||||
tcc.save()
|
||||
|
||||
f = FollowUp(
|
||||
ticket=t,
|
||||
title=_('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}),
|
||||
date=timezone.now(),
|
||||
public=True,
|
||||
comment=body,
|
||||
)
|
||||
|
||||
if t.status == Ticket.REOPENED_STATUS:
|
||||
f.new_status = Ticket.REOPENED_STATUS
|
||||
f.title = _('Ticket Re-Opened by E-Mail Received from %(sender_email)s' % {'sender_email': sender_email})
|
||||
|
||||
f.save()
|
||||
logger.debug("Created new FollowUp for Ticket")
|
||||
|
||||
if six.PY2:
|
||||
logger.info(("[%s-%s] %s" % (t.queue.slug, t.id, t.title,)).encode('ascii', 'replace'))
|
||||
elif six.PY3:
|
||||
logger.info("[%s-%s] %s" % (t.queue.slug, t.id, t.title,))
|
||||
|
||||
attached = process_attachments(f, files)
|
||||
for att_file in attached:
|
||||
logger.info("Attachment '%s' (with size %s) successfully added to ticket from email." % (att_file[0], att_file[1].size))
|
||||
|
||||
context = safe_template_context(t)
|
||||
|
||||
if new:
|
||||
if sender_email:
|
||||
send_templated_mail(
|
||||
'newticket_submitter',
|
||||
context,
|
||||
recipients=sender_email,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
if queue.new_ticket_cc:
|
||||
send_templated_mail(
|
||||
'newticket_cc',
|
||||
context,
|
||||
recipients=queue.new_ticket_cc,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
if queue.updated_ticket_cc and queue.updated_ticket_cc != queue.new_ticket_cc:
|
||||
send_templated_mail(
|
||||
'newticket_cc',
|
||||
context,
|
||||
recipients=queue.updated_ticket_cc,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
else:
|
||||
context.update(comment=f.comment)
|
||||
if t.assigned_to:
|
||||
send_templated_mail(
|
||||
'updated_owner',
|
||||
context,
|
||||
recipients=t.assigned_to.email,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
if queue.updated_ticket_cc:
|
||||
send_templated_mail(
|
||||
'updated_cc',
|
||||
context,
|
||||
recipients=queue.updated_ticket_cc,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
return t
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
process_email()
|
349
build/lib/helpdesk/migrations/0001_initial.py
Normal file
349
build/lib/helpdesk/migrations/0001_initial.py
Normal file
@ -0,0 +1,349 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
import helpdesk.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Attachment',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('file', models.FileField(upload_to=helpdesk.models.attachment_path, verbose_name='File', max_length=1000)),
|
||||
('filename', models.CharField(verbose_name='Filename', max_length=1000)),
|
||||
('mime_type', models.CharField(verbose_name='MIME Type', max_length=255)),
|
||||
('size', models.IntegerField(verbose_name='Size', help_text='Size of this file in bytes')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Attachments',
|
||||
'verbose_name': 'Attachment',
|
||||
'ordering': ['filename'],
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomField',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('name', models.SlugField(help_text='As used in the database and behind the scenes. Must be unique and consist of only lowercase letters with no punctuation.', verbose_name='Field Name', unique=True)),
|
||||
('label', models.CharField(verbose_name='Label', help_text='The display label for this field', max_length='30')),
|
||||
('help_text', models.TextField(null=True, verbose_name='Help Text', blank=True, help_text='Shown to the user when editing the ticket')),
|
||||
('data_type', models.CharField(choices=[('varchar', 'Character (single line)'), ('text', 'Text (multi-line)'), ('integer', 'Integer'), ('decimal', 'Decimal'), ('list', 'List'), ('boolean', 'Boolean (checkbox yes/no)'), ('date', 'Date'), ('time', 'Time'), ('datetime', 'Date & Time'), ('email', 'E-Mail Address'), ('url', 'URL'), ('ipaddress', 'IP Address'), ('slug', 'Slug')], verbose_name='Data Type', help_text='Allows you to restrict the data entered into this field', max_length=100)),
|
||||
('max_length', models.IntegerField(null=True, verbose_name='Maximum Length (characters)', blank=True)),
|
||||
('decimal_places', models.IntegerField(null=True, verbose_name='Decimal Places', blank=True, help_text='Only used for decimal fields')),
|
||||
('empty_selection_list', models.BooleanField(verbose_name='Add empty first choice to List?', default=False, help_text='Only for List: adds an empty first entry to the choices list, which enforces that the user makes an active choice.')),
|
||||
('list_values', models.TextField(null=True, verbose_name='List Values', blank=True, help_text='For list fields only. Enter one option per line.')),
|
||||
('ordering', models.IntegerField(null=True, verbose_name='Ordering', blank=True, help_text='Lower numbers are displayed first; higher numbers are listed later')),
|
||||
('required', models.BooleanField(verbose_name='Required?', default=False, help_text='Does the user have to enter a value for this field?')),
|
||||
('staff_only', models.BooleanField(verbose_name='Staff Only?', default=False, help_text='If this is ticked, then the public submission form will NOT show this field')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Custom fields',
|
||||
'verbose_name': 'Custom field',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EmailTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('template_name', models.CharField(verbose_name='Template Name', max_length=100)),
|
||||
('subject', models.CharField(verbose_name='Subject', help_text='This will be prefixed with "[ticket.ticket] ticket.title". We recommend something simple such as "(Updated") or "(Closed)" - the same context is available as in plain_text, below.', max_length=100)),
|
||||
('heading', models.CharField(verbose_name='Heading', help_text='In HTML e-mails, this will be the heading at the top of the email - the same context is available as in plain_text, below.', max_length=100)),
|
||||
('plain_text', models.TextField(verbose_name='Plain Text', help_text='The context available to you includes {{ ticket }}, {{ queue }}, and depending on the time of the call: {{ resolution }} or {{ comment }}.')),
|
||||
('html', models.TextField(verbose_name='HTML', help_text='The same context is available here as in plain_text, above.')),
|
||||
('locale', models.CharField(null=True, verbose_name='Locale', help_text='Locale of this template.', blank=True, max_length=10)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'e-mail templates',
|
||||
'verbose_name': 'e-mail template',
|
||||
'ordering': ['template_name', 'locale'],
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EscalationExclusion',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('name', models.CharField(verbose_name='Name', max_length=100)),
|
||||
('date', models.DateField(verbose_name='Date', help_text='Date on which escalation should not happen')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Escalation exclusions',
|
||||
'verbose_name': 'Escalation exclusion',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FollowUp',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('date', models.DateTimeField(verbose_name='Date', default=django.utils.timezone.now)),
|
||||
('title', models.CharField(null=True, verbose_name='Title', blank=True, max_length=200)),
|
||||
('comment', models.TextField(null=True, verbose_name='Comment', blank=True)),
|
||||
('public', models.BooleanField(verbose_name='Public', default=False, help_text='Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.')),
|
||||
('new_status', models.IntegerField(null=True, verbose_name='New Status', help_text='If the status was changed, what was it changed to?', blank=True, choices=[(1, 'Open'), (2, 'Reopened'), (3, 'Resolved'), (4, 'Closed'), (5, 'Duplicate')])),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Follow-ups',
|
||||
'verbose_name': 'Follow-up',
|
||||
'ordering': ['date'],
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IgnoreEmail',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('name', models.CharField(verbose_name='Name', max_length=100)),
|
||||
('date', models.DateField(editable=False, verbose_name='Date', blank=True, help_text='Date on which this e-mail address was added')),
|
||||
('email_address', models.CharField(verbose_name='E-Mail Address', help_text='Enter a full e-mail address, or portions with wildcards, eg *@domain.com or postmaster@*.', max_length=150)),
|
||||
('keep_in_mailbox', models.BooleanField(verbose_name='Save Emails in Mailbox?', 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.')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Ignored e-mail addresses',
|
||||
'verbose_name': 'Ignored e-mail address',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='KBCategory',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('title', models.CharField(verbose_name='Title', max_length=100)),
|
||||
('slug', models.SlugField(verbose_name='Slug')),
|
||||
('description', models.TextField(verbose_name='Description')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Knowledge base categories',
|
||||
'verbose_name': 'Knowledge base category',
|
||||
'ordering': ['title'],
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='KBItem',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('title', models.CharField(verbose_name='Title', max_length=100)),
|
||||
('question', models.TextField(verbose_name='Question')),
|
||||
('answer', models.TextField(verbose_name='Answer')),
|
||||
('votes', models.IntegerField(verbose_name='Votes', default=0, help_text='Total number of votes cast for this item')),
|
||||
('recommendations', models.IntegerField(verbose_name='Positive Votes', default=0, help_text='Number of votes for this item which were POSITIVE.')),
|
||||
('last_updated', models.DateTimeField(verbose_name='Last Updated', blank=True, help_text='The date on which this question was most recently changed.')),
|
||||
('category', models.ForeignKey(verbose_name='Category', to='helpdesk.KBCategory', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Knowledge base items',
|
||||
'verbose_name': 'Knowledge base item',
|
||||
'ordering': ['title'],
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PreSetReply',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('name', models.CharField(verbose_name='Name', help_text='Only used to assist users with selecting a reply - not shown to the user.', max_length=100)),
|
||||
('body', models.TextField(verbose_name='Body', help_text='Context available: {{ ticket }} - ticket object (eg {{ ticket.title }}); {{ queue }} - The queue; and {{ user }} - the current user.')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Pre-set replies',
|
||||
'verbose_name': 'Pre-set reply',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Queue',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('title', models.CharField(verbose_name='Title', max_length=100)),
|
||||
('slug', models.SlugField(verbose_name='Slug', help_text="This slug is used when building ticket ID's. Once set, try not to change it or e-mailing may get messy.")),
|
||||
('email_address', models.EmailField(null=True, verbose_name='E-Mail Address', 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.', blank=True, max_length=75)),
|
||||
('locale', models.CharField(null=True, verbose_name='Locale', help_text='Locale of this queue. All correspondence in this queue will be in this language.', blank=True, max_length=10)),
|
||||
('allow_public_submission', models.BooleanField(verbose_name='Allow Public Submission?', default=False, help_text='Should this queue be listed on the public submission form?')),
|
||||
('allow_email_submission', models.BooleanField(verbose_name='Allow E-Mail Submission?', default=False, help_text='Do you want to poll the e-mail box below for new tickets?')),
|
||||
('escalate_days', models.IntegerField(null=True, verbose_name='Escalation Days', blank=True, help_text='For tickets which are not held, how often do you wish to increase their priority? Set to 0 for no escalation.')),
|
||||
('new_ticket_cc', models.CharField(null=True, verbose_name='New Ticket CC Address', help_text='If an e-mail address is entered here, then it will receive notification of all new tickets created for this queue. Enter a comma between multiple e-mail addresses.', blank=True, max_length=200)),
|
||||
('updated_ticket_cc', models.CharField(null=True, verbose_name='Updated Ticket CC Address', help_text='If an e-mail address is entered here, then it will receive notification of all activity (new tickets, closed tickets, updates, reassignments, etc) for this queue. Separate multiple addresses with a comma.', blank=True, max_length=200)),
|
||||
('email_box_type', models.CharField(null=True, verbose_name='E-Mail Box Type', help_text='E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported.', blank=True, max_length=5, choices=[('pop3', 'POP 3'), ('imap', 'IMAP')])),
|
||||
('email_box_host', models.CharField(null=True, verbose_name='E-Mail Hostname', help_text='Your e-mail server address - either the domain name or IP address. May be "localhost".', blank=True, max_length=200)),
|
||||
('email_box_port', models.IntegerField(null=True, verbose_name='E-Mail Port', blank=True, help_text='Port number to use for accessing e-mail. Default for POP3 is "110", and for IMAP is "143". This may differ on some servers. Leave it blank to use the defaults.')),
|
||||
('email_box_ssl', models.BooleanField(verbose_name='Use SSL for E-Mail?', 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.')),
|
||||
('email_box_user', models.CharField(null=True, verbose_name='E-Mail Username', help_text='Username for accessing this mailbox.', blank=True, max_length=200)),
|
||||
('email_box_pass', models.CharField(null=True, verbose_name='E-Mail Password', help_text='Password for the above username', blank=True, max_length=200)),
|
||||
('email_box_imap_folder', models.CharField(null=True, verbose_name='IMAP Folder', help_text='If using IMAP, what folder do you wish to fetch messages from? This allows you to use one IMAP account for multiple queues, by filtering messages on your IMAP server into separate folders. Default: INBOX.', blank=True, max_length=100)),
|
||||
('email_box_interval', models.IntegerField(null=True, verbose_name='E-Mail Check Interval', blank=True, default='5', help_text='How often do you wish to check this mailbox? (in Minutes)')),
|
||||
('email_box_last_check', models.DateTimeField(editable=False, null=True, blank=True)),
|
||||
('socks_proxy_type', models.CharField(null=True, verbose_name='Socks Proxy Type', help_text='SOCKS4 or SOCKS5 allows you to proxy your connections through a SOCKS server.', blank=True, max_length=8, choices=[('socks4', 'SOCKS4'), ('socks5', 'SOCKS5')])),
|
||||
('socks_proxy_host', models.GenericIPAddressField(null=True, verbose_name='Socks Proxy Host', help_text='Socks proxy IP address. Default: 127.0.0.1', blank=True)),
|
||||
('socks_proxy_port', models.IntegerField(null=True, verbose_name='Socks Proxy Port', blank=True, help_text='Socks proxy port number. Default: 9150 (default TOR port)')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Queues',
|
||||
'verbose_name': 'Queue',
|
||||
'ordering': ('title',),
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SavedSearch',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('title', models.CharField(verbose_name='Query Name', help_text='User-provided name for this query', max_length=100)),
|
||||
('shared', models.BooleanField(verbose_name='Shared With Other Users?', default=False, help_text='Should other users see this query?')),
|
||||
('query', models.TextField(verbose_name='Search Query', help_text='Pickled query object. Be wary changing this.')),
|
||||
('user', models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Saved searches',
|
||||
'verbose_name': 'Saved search',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Ticket',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('title', models.CharField(verbose_name='Title', max_length=200)),
|
||||
('created', models.DateTimeField(verbose_name='Created', blank=True, help_text='Date this ticket was first created')),
|
||||
('modified', models.DateTimeField(verbose_name='Modified', blank=True, help_text='Date this ticket was most recently changed.')),
|
||||
('submitter_email', models.EmailField(null=True, verbose_name='Submitter E-Mail', help_text='The submitter will receive an email for all public follow-ups left for this task.', blank=True, max_length=75)),
|
||||
('status', models.IntegerField(verbose_name='Status', default=1, choices=[(1, 'Open'), (2, 'Reopened'), (3, 'Resolved'), (4, 'Closed'), (5, 'Duplicate')])),
|
||||
('on_hold', models.BooleanField(verbose_name='On Hold', default=False, help_text='If a ticket is on hold, it will not automatically be escalated.')),
|
||||
('description', models.TextField(null=True, verbose_name='Description', blank=True, help_text='The content of the customers query.')),
|
||||
('resolution', models.TextField(null=True, verbose_name='Resolution', blank=True, help_text='The resolution provided to the customer by our staff.')),
|
||||
('priority', models.IntegerField(verbose_name='Priority', help_text='1 = Highest Priority, 5 = Low Priority', blank=3, default=3, choices=[(1, '1. Critical'), (2, '2. High'), (3, '3. Normal'), (4, '4. Low'), (5, '5. Very Low')])),
|
||||
('due_date', models.DateTimeField(null=True, verbose_name='Due on', blank=True)),
|
||||
('last_escalation', models.DateTimeField(editable=False, null=True, blank=True, help_text='The date this ticket was last escalated - updated automatically by management/commands/escalate_tickets.py.')),
|
||||
('assigned_to', models.ForeignKey(null=True, verbose_name='Assigned to', blank=True, related_name='assigned_to', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
('queue', models.ForeignKey(verbose_name='Queue', to='helpdesk.Queue', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Tickets',
|
||||
'verbose_name': 'Ticket',
|
||||
'ordering': ('id',),
|
||||
'get_latest_by': 'created',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketCC',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('email', models.EmailField(null=True, verbose_name='E-Mail Address', help_text='For non-user followers, enter their e-mail address', blank=True, max_length=75)),
|
||||
('can_view', models.BooleanField(verbose_name='Can View Ticket?', default=False, help_text='Can this CC login to view the ticket details?')),
|
||||
('can_update', models.BooleanField(verbose_name='Can Update Ticket?', default=False, help_text='Can this CC login and update the ticket?')),
|
||||
('ticket', models.ForeignKey(verbose_name='Ticket', to='helpdesk.Ticket', on_delete=models.CASCADE)),
|
||||
('user', models.ForeignKey(null=True, verbose_name='User', blank=True, to=settings.AUTH_USER_MODEL, help_text='User who wishes to receive updates for this ticket.', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketChange',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('field', models.CharField(verbose_name='Field', max_length=100)),
|
||||
('old_value', models.TextField(null=True, verbose_name='Old Value', blank=True)),
|
||||
('new_value', models.TextField(null=True, verbose_name='New Value', blank=True)),
|
||||
('followup', models.ForeignKey(verbose_name='Follow-up', to='helpdesk.FollowUp', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Ticket changes',
|
||||
'verbose_name': 'Ticket change',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketCustomFieldValue',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('value', models.TextField(null=True, blank=True)),
|
||||
('field', models.ForeignKey(verbose_name='Field', to='helpdesk.CustomField', on_delete=models.CASCADE)),
|
||||
('ticket', models.ForeignKey(verbose_name='Ticket', to='helpdesk.Ticket', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Ticket custom field values',
|
||||
'verbose_name': 'Ticket custom field value',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketDependency',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('depends_on', models.ForeignKey(related_name='depends_on', verbose_name='Depends On Ticket', to='helpdesk.Ticket', on_delete=models.CASCADE)),
|
||||
('ticket', models.ForeignKey(related_name='ticketdependency', verbose_name='Ticket', to='helpdesk.Ticket', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Ticket dependencies',
|
||||
'verbose_name': 'Ticket dependency',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserSettings',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
|
||||
('settings_pickled', models.TextField(null=True, verbose_name='Settings Dictionary', blank=True, help_text='This is a base64-encoded representation of a pickled Python dictionary. Do not change this field via the admin.')),
|
||||
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'User Settings',
|
||||
'verbose_name': 'User Setting',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='ticketdependency',
|
||||
unique_together=set([('ticket', 'depends_on')]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='presetreply',
|
||||
name='queues',
|
||||
field=models.ManyToManyField(null=True, to='helpdesk.Queue', blank=True, 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.'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ignoreemail',
|
||||
name='queues',
|
||||
field=models.ManyToManyField(null=True, to='helpdesk.Queue', blank=True, 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.'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='followup',
|
||||
name='ticket',
|
||||
field=models.ForeignKey(verbose_name='Ticket', to='helpdesk.Ticket', on_delete=models.CASCADE),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='followup',
|
||||
name='user',
|
||||
field=models.ForeignKey(null=True, verbose_name='User', blank=True, to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='escalationexclusion',
|
||||
name='queues',
|
||||
field=models.ManyToManyField(null=True, to='helpdesk.Queue', blank=True, help_text='Leave blank for this exclusion to be applied to all queues, or select those queues you wish to exclude with this entry.'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='attachment',
|
||||
name='followup',
|
||||
field=models.ForeignKey(verbose_name='Follow-up', to='helpdesk.FollowUp', on_delete=models.CASCADE),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
53
build/lib/helpdesk/migrations/0002_populate_usersettings.py
Normal file
53
build/lib/helpdesk/migrations/0002_populate_usersettings.py
Normal file
@ -0,0 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models, migrations
|
||||
|
||||
from helpdesk.settings import DEFAULT_USER_SETTINGS
|
||||
|
||||
|
||||
def picke_settings(data):
|
||||
"""Pickling as defined at migration's creation time"""
|
||||
try:
|
||||
import pickle
|
||||
except ImportError:
|
||||
import cPickle as pickle
|
||||
from helpdesk.lib import b64encode
|
||||
return b64encode(pickle.dumps(data))
|
||||
|
||||
|
||||
# https://docs.djangoproject.com/en/1.7/topics/migrations/#data-migrations
|
||||
def populate_usersettings(apps, schema_editor):
|
||||
"""Create a UserSettings entry for each existing user.
|
||||
This will only happen once (at install time, or at upgrade)
|
||||
when the UserSettings model doesn't already exist."""
|
||||
|
||||
_User = get_user_model()
|
||||
User = apps.get_model(_User._meta.app_label, _User._meta.model_name)
|
||||
|
||||
# Import historical version of models
|
||||
UserSettings = apps.get_model("helpdesk", "UserSettings")
|
||||
|
||||
settings_pickled = picke_settings(DEFAULT_USER_SETTINGS)
|
||||
|
||||
for u in User.objects.all():
|
||||
try:
|
||||
UserSettings.objects.get(user=u)
|
||||
except UserSettings.DoesNotExist:
|
||||
UserSettings.objects.create(user=u, settings_pickled=settings_pickled)
|
||||
|
||||
|
||||
noop = lambda *args, **kwargs: None
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(populate_usersettings, reverse_code=noop),
|
||||
]
|
||||
|
||||
|
44
build/lib/helpdesk/migrations/0003_initial_data_import.py
Normal file
44
build/lib/helpdesk/migrations/0003_initial_data_import.py
Normal file
@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
from sys import path
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.core import serializers
|
||||
|
||||
fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures'))
|
||||
fixture_filename = 'emailtemplate.json'
|
||||
|
||||
def deserialize_fixture():
|
||||
fixture_file = os.path.join(fixture_dir, fixture_filename)
|
||||
|
||||
with open(fixture_file, 'rb') as fixture:
|
||||
return list(serializers.deserialize('json', fixture, ignorenonexistent=True))
|
||||
|
||||
|
||||
def load_fixture(apps, schema_editor):
|
||||
objects = deserialize_fixture()
|
||||
|
||||
for obj in objects:
|
||||
obj.save()
|
||||
|
||||
|
||||
def unload_fixture(apps, schema_editor):
|
||||
"""Delete all EmailTemplate objects"""
|
||||
|
||||
objects = deserialize_fixture()
|
||||
|
||||
EmailTemplate = apps.get_model("helpdesk", "emailtemplate")
|
||||
EmailTemplate.objects.filter(pk__in=[ obj.object.pk for obj in objects ]).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0002_populate_usersettings'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(load_fixture, reverse_code=unload_fixture),
|
||||
]
|
@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('helpdesk', '0003_initial_data_import'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='QueueMembership',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('queues', models.ManyToManyField(to='helpdesk.Queue', verbose_name='Authorized Queues')),
|
||||
('user', models.OneToOneField(verbose_name='User', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Queue Membership',
|
||||
'verbose_name_plural': 'Queue Memberships',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
29
build/lib/helpdesk/migrations/0005_queues_no_null.py
Normal file
29
build/lib/helpdesk/migrations/0005_queues_no_null.py
Normal file
@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0004_add_per_queue_staff_membership'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='escalationexclusion',
|
||||
name='queues',
|
||||
field=models.ManyToManyField(help_text='Leave blank for this exclusion to be applied to all queues, or select those queues you wish to exclude with this entry.', to='helpdesk.Queue', blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ignoreemail',
|
||||
name='queues',
|
||||
field=models.ManyToManyField(help_text='Leave blank for this e-mail to be ignored on all queues, or select those queues you wish to ignore this e-mail for.', to='helpdesk.Queue', blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='presetreply',
|
||||
name='queues',
|
||||
field=models.ManyToManyField(help_text='Leave blank to allow this reply to be used for all queues, or select those queues you wish to limit this reply to.', to='helpdesk.Queue', blank=True),
|
||||
),
|
||||
]
|
29
build/lib/helpdesk/migrations/0006_email_maxlength.py
Normal file
29
build/lib/helpdesk/migrations/0006_email_maxlength.py
Normal file
@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0005_queues_no_null'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='email_address',
|
||||
field=models.EmailField(help_text='All outgoing e-mails for this queue will use this e-mail address. If you use IMAP or POP3, this should be the e-mail address for that mailbox.', max_length=254, null=True, verbose_name='E-Mail Address', blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticket',
|
||||
name='submitter_email',
|
||||
field=models.EmailField(help_text='The submitter will receive an email for all public follow-ups left for this task.', max_length=254, null=True, verbose_name='Submitter E-Mail', blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticketcc',
|
||||
name='email',
|
||||
field=models.EmailField(help_text='For non-user followers, enter their e-mail address', max_length=254, null=True, verbose_name='E-Mail Address', blank=True),
|
||||
),
|
||||
]
|
19
build/lib/helpdesk/migrations/0007_max_length_by_integer.py
Normal file
19
build/lib/helpdesk/migrations/0007_max_length_by_integer.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0006_email_maxlength'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='label',
|
||||
field=models.CharField(help_text='The display label for this field', max_length=30, verbose_name='Label'),
|
||||
),
|
||||
]
|
19
build/lib/helpdesk/migrations/0008_extra_for_permissions.py
Normal file
19
build/lib/helpdesk/migrations/0008_extra_for_permissions.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0007_max_length_by_integer'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='queue',
|
||||
name='permission_name',
|
||||
field=models.CharField(help_text='Name used in the django.contrib.auth permission system', max_length=50, null=True, verbose_name='Django auth permission name', blank=True),
|
||||
),
|
||||
]
|
@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import migrations
|
||||
from django.db.utils import IntegrityError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
def create_and_assign_permissions(apps, schema_editor):
|
||||
Permission = apps.get_model('auth', 'Permission')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
# Two steps:
|
||||
# 1. Create the permission for existing Queues
|
||||
# 2. Assign the permission to user according to QueueMembership objects
|
||||
|
||||
# First step: prepare the permission for each queue
|
||||
Queue = apps.get_model('helpdesk', 'Queue')
|
||||
|
||||
for q in Queue.objects.all():
|
||||
if not q.permission_name:
|
||||
basename = "queue_access_%s" % q.slug
|
||||
q.permission_name = "helpdesk.%s" % basename
|
||||
else:
|
||||
# Strip the `helpdesk.` prefix
|
||||
basename = q.permission_name[9:]
|
||||
|
||||
try:
|
||||
Permission.objects.create(
|
||||
name=_("Permission for queue: ") + q.title,
|
||||
content_type=ContentType.objects.get(model="queue"),
|
||||
codename=basename,
|
||||
)
|
||||
except IntegrityError:
|
||||
# Seems that it already existed, safely ignore it
|
||||
pass
|
||||
q.save()
|
||||
|
||||
# Second step: map the permissions according to QueueMembership
|
||||
QueueMembership = apps.get_model('helpdesk', 'QueueMembership')
|
||||
for qm in QueueMembership.objects.all():
|
||||
user = qm.user
|
||||
for q in qm.queues.all():
|
||||
# Strip the `helpdesk.` prefix
|
||||
p = Permission.objects.get(codename=q.permission_name[9:])
|
||||
user.user_permissions.add(p)
|
||||
qm.delete()
|
||||
|
||||
|
||||
def revert_queue_membership(apps, schema_editor):
|
||||
Permission = apps.get_model('auth', 'Permission')
|
||||
Queue = apps.get_model('helpdesk', 'Queue')
|
||||
QueueMembership = apps.get_model('helpdesk', 'QueueMembership')
|
||||
for p in Permission.objects.all():
|
||||
if p.codename.startswith("queue_access_"):
|
||||
slug = p.codename[13:]
|
||||
try:
|
||||
q = Queue.objects.get(slug=slug)
|
||||
except ObjectDoesNotExist:
|
||||
continue
|
||||
|
||||
for user in p.user_set.all():
|
||||
qm, _ = QueueMembership.objects.get_or_create(user=user)
|
||||
qm.queues.add(q)
|
||||
|
||||
p.delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0008_extra_for_permissions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_and_assign_permissions,
|
||||
revert_queue_membership)
|
||||
]
|
25
build/lib/helpdesk/migrations/0010_remove_queuemembership.py
Normal file
25
build/lib/helpdesk/migrations/0010_remove_queuemembership.py
Normal file
@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0009_migrate_queuemembership'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='queuemembership',
|
||||
name='queues',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='queuemembership',
|
||||
name='user',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='QueueMembership',
|
||||
),
|
||||
]
|
@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0010_remove_queuemembership'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='permission_name',
|
||||
field=models.CharField(editable=False, max_length=50, blank=True, help_text='Name used in the django.contrib.auth permission system', null=True, verbose_name='Django auth permission name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text="This slug is used when building ticket ID's. Once set, try not to change it or e-mailing may get messy.", unique=True, verbose_name='Slug'),
|
||||
),
|
||||
]
|
23
build/lib/helpdesk/migrations/0012_queue_default_owner.py
Normal file
23
build/lib/helpdesk/migrations/0012_queue_default_owner.py
Normal file
@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.2 on 2016-02-15 21:37
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('helpdesk', '0011_admin_related_improvements'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='queue',
|
||||
name='default_owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='default_owner', to=settings.AUTH_USER_MODEL, verbose_name='Default owner'),
|
||||
),
|
||||
]
|
@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.1 on 2016-09-14 23:47
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0012_queue_default_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='queue',
|
||||
name='email_box_local_dir',
|
||||
field=models.CharField(blank=True, help_text='If using a local directory, what directory path do you wish to poll for new email? Example: /var/lib/mail/helpdesk/', max_length=200, null=True, verbose_name='E-Mail Local Directory'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='queue',
|
||||
name='logging_dir',
|
||||
field=models.CharField(blank=True, help_text='If logging is enabled, what directory should we use to store log files for this queue? If no directory is set, default to /var/log/helpdesk/', max_length=200, null=True, verbose_name='Logging Directory'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='queue',
|
||||
name='logging_type',
|
||||
field=models.CharField(blank=True, choices=[('none', 'None'), ('debug', 'Debug'), ('info', 'Information'), ('warn', 'Warning'), ('error', 'Error'), ('crit', 'Critical')], help_text='Set the default logging level. All messages at that level or above will be logged to the directory set below. If no level is set, logging will be disabled.', max_length=5, null=True, verbose_name='Logging Type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='email_box_type',
|
||||
field=models.CharField(blank=True, choices=[('pop3', 'POP 3'), ('imap', 'IMAP'), ('local', 'Local Directory')], help_text='E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.', max_length=5, null=True, verbose_name='E-Mail Box Type'),
|
||||
),
|
||||
]
|
@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0013_email_box_local_dir_and_logging'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='usersettings',
|
||||
name='user',
|
||||
field=models.OneToOneField(to=settings.AUTH_USER_MODEL,
|
||||
related_name='usersettings_helpdesk',
|
||||
on_delete=models.CASCADE),
|
||||
),
|
||||
]
|
@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.5 on 2017-02-10 19:27
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0014_usersettings_related_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='permission_name',
|
||||
field=models.CharField(blank=True, editable=False, help_text='Name used in the django.contrib.auth permission system', max_length=72, null=True, verbose_name='Django auth permission name'),
|
||||
),
|
||||
]
|
43
build/lib/helpdesk/migrations/0016_alter_model_options.py
Normal file
43
build/lib/helpdesk/migrations/0016_alter_model_options.py
Normal file
@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.2 on 2017-03-08 17:51
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0015_expand_permission_name_size'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='attachment',
|
||||
options={'ordering': ('filename',), 'verbose_name': 'Attachment', 'verbose_name_plural': 'Attachments'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='emailtemplate',
|
||||
options={'ordering': ('template_name', 'locale'), 'verbose_name': 'e-mail template', 'verbose_name_plural': 'e-mail templates'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='followup',
|
||||
options={'ordering': ('date',), 'verbose_name': 'Follow-up', 'verbose_name_plural': 'Follow-ups'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='kbcategory',
|
||||
options={'ordering': ('title',), 'verbose_name': 'Knowledge base category', 'verbose_name_plural': 'Knowledge base categories'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='kbitem',
|
||||
options={'ordering': ('title',), 'verbose_name': 'Knowledge base item', 'verbose_name_plural': 'Knowledge base items'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='presetreply',
|
||||
options={'ordering': ('name',), 'verbose_name': 'Pre-set reply', 'verbose_name_plural': 'Pre-set replies'},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='ticketcustomfieldvalue',
|
||||
unique_together=set([('ticket', 'field')]),
|
||||
),
|
||||
]
|
@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2018-01-19 09:48
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0016_alter_model_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='default_owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_owner', to=settings.AUTH_USER_MODEL, verbose_name='Default owner'),
|
||||
),
|
||||
]
|
0
build/lib/helpdesk/migrations/__init__.py
Normal file
0
build/lib/helpdesk/migrations/__init__.py
Normal file
1502
build/lib/helpdesk/models.py
Normal file
1502
build/lib/helpdesk/models.py
Normal file
File diff suppressed because it is too large
Load Diff
18
build/lib/helpdesk/poll_helpdesk_email_queues.sh
Normal file
18
build/lib/helpdesk/poll_helpdesk_email_queues.sh
Normal file
@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
|
||||
# don't forget to add this script to the /etc/crontab:
|
||||
#
|
||||
# */1 * * * * username /home/username/django/project/poll_helpdesk_email_queues.sh >> /tmp/foo.log 2>&1
|
||||
|
||||
# set your django and project paths here
|
||||
PATHTODJANGO="/home/username/django/libraries/lib/python"
|
||||
PATHTOPROJECT="/home/username/django/project/"
|
||||
|
||||
|
||||
export PYTHONPATH=$PYTHONPATH:$PATHTODJANGO:$PATHTOPROJECT:
|
||||
|
||||
cd $PATHTOPROJECT
|
||||
/usr/bin/python manage.py get_email
|
||||
|
||||
|
151
build/lib/helpdesk/settings.py
Normal file
151
build/lib/helpdesk/settings.py
Normal file
@ -0,0 +1,151 @@
|
||||
"""
|
||||
Default settings for django-helpdesk.
|
||||
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
try:
|
||||
DEFAULT_USER_SETTINGS = settings.HELPDESK_DEFAULT_SETTINGS
|
||||
except AttributeError:
|
||||
DEFAULT_USER_SETTINGS = None
|
||||
|
||||
if not isinstance(DEFAULT_USER_SETTINGS, dict):
|
||||
DEFAULT_USER_SETTINGS = {
|
||||
'use_email_as_submitter': True,
|
||||
'email_on_ticket_assign': True,
|
||||
'email_on_ticket_change': True,
|
||||
'login_view_ticketlist': True,
|
||||
'tickets_per_page': 25
|
||||
}
|
||||
|
||||
|
||||
HAS_TAG_SUPPORT = False
|
||||
|
||||
##########################################
|
||||
# generic options - visible on all pages #
|
||||
##########################################
|
||||
|
||||
# redirect to login page instead of the default homepage when users visits "/"?
|
||||
HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(settings,
|
||||
'HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT',
|
||||
False)
|
||||
|
||||
# raises a 404 to anon users. It's like it was invisible
|
||||
HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings,
|
||||
'HELPDESK_ANON_ACCESS_RAISES_404',
|
||||
False)
|
||||
|
||||
# show knowledgebase links?
|
||||
HELPDESK_KB_ENABLED = getattr(settings, 'HELPDESK_KB_ENABLED', True)
|
||||
|
||||
# show extended navigation by default, to all users, irrespective of staff status?
|
||||
HELPDESK_NAVIGATION_ENABLED = getattr(settings, 'HELPDESK_NAVIGATION_ENABLED', False)
|
||||
|
||||
# use public CDNs to serve jquery and other javascript by default?
|
||||
# otherwise, use built-in static copy
|
||||
HELPDESK_USE_CDN = getattr(settings, 'HELPDESK_USE_CDN', False)
|
||||
|
||||
# show dropdown list of languages that ticket comments can be translated into?
|
||||
HELPDESK_TRANSLATE_TICKET_COMMENTS = getattr(settings,
|
||||
'HELPDESK_TRANSLATE_TICKET_COMMENTS',
|
||||
False)
|
||||
|
||||
# list of languages to offer. if set to false,
|
||||
# all default google translate languages will be shown.
|
||||
HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG = getattr(settings,
|
||||
'HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG',
|
||||
["en", "de", "es", "fr", "it", "ru"])
|
||||
|
||||
# show link to 'change password' on 'User Settings' page?
|
||||
HELPDESK_SHOW_CHANGE_PASSWORD = getattr(settings, 'HELPDESK_SHOW_CHANGE_PASSWORD', False)
|
||||
|
||||
# allow user to override default layout for 'followups' - work in progress.
|
||||
HELPDESK_FOLLOWUP_MOD = getattr(settings, 'HELPDESK_FOLLOWUP_MOD', False)
|
||||
|
||||
# auto-subscribe user to ticket if (s)he responds to a ticket?
|
||||
HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE = getattr(settings,
|
||||
'HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE',
|
||||
False)
|
||||
|
||||
|
||||
############################
|
||||
# options for public pages #
|
||||
############################
|
||||
|
||||
# show 'view a ticket' section on public page?
|
||||
HELPDESK_VIEW_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC', True)
|
||||
|
||||
# show 'submit a ticket' section on public page?
|
||||
HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_SUBMIT_A_TICKET_PUBLIC', True)
|
||||
|
||||
|
||||
###################################
|
||||
# options for update_ticket views #
|
||||
###################################
|
||||
|
||||
# allow non-staff users to interact with tickets?
|
||||
# this will also change how 'staff_member_required'
|
||||
# in staff.py will be defined.
|
||||
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(settings,
|
||||
'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE',
|
||||
False)
|
||||
|
||||
# show edit buttons in ticket follow ups.
|
||||
HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(settings,
|
||||
'HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP',
|
||||
True)
|
||||
|
||||
# show delete buttons in ticket follow ups if user is 'superuser'
|
||||
HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP = getattr(
|
||||
settings, 'HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP', False)
|
||||
|
||||
# make all updates public by default? this will hide the 'is this update public' checkbox
|
||||
HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr(settings, 'HELPDESK_UPDATE_PUBLIC_DEFAULT', False)
|
||||
|
||||
# only show staff users in ticket owner drop-downs
|
||||
HELPDESK_STAFF_ONLY_TICKET_OWNERS = getattr(settings, 'HELPDESK_STAFF_ONLY_TICKET_OWNERS', False)
|
||||
|
||||
# only show staff users in ticket cc drop-down
|
||||
HELPDESK_STAFF_ONLY_TICKET_CC = getattr(settings, 'HELPDESK_STAFF_ONLY_TICKET_CC', False)
|
||||
|
||||
# allow the subject to have a configurable template.
|
||||
HELPDESK_EMAIL_SUBJECT_TEMPLATE = getattr(
|
||||
settings, 'HELPDESK_EMAIL_SUBJECT_TEMPLATE',
|
||||
"{{ ticket.ticket }} {{ ticket.title|safe }} %(subject)s")
|
||||
# since django-helpdesk may not work correctly without the ticket ID
|
||||
# in the subject, let's do a check for it quick:
|
||||
if HELPDESK_EMAIL_SUBJECT_TEMPLATE.find("ticket.ticket") < 0:
|
||||
raise ImproperlyConfigured
|
||||
|
||||
# default fallback locale when queue locale not found
|
||||
HELPDESK_EMAIL_FALLBACK_LOCALE = getattr(settings, 'HELPDESK_EMAIL_FALLBACK_LOCALE', 'en')
|
||||
|
||||
|
||||
########################################
|
||||
# options for staff.create_ticket view #
|
||||
########################################
|
||||
|
||||
# hide the 'assigned to' / 'Case owner' field from the 'create_ticket' view?
|
||||
HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr(
|
||||
settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False)
|
||||
|
||||
|
||||
#################
|
||||
# email options #
|
||||
#################
|
||||
|
||||
# default Queue email submission settings
|
||||
QUEUE_EMAIL_BOX_TYPE = getattr(settings, 'QUEUE_EMAIL_BOX_TYPE', None)
|
||||
QUEUE_EMAIL_BOX_SSL = getattr(settings, 'QUEUE_EMAIL_BOX_SSL', None)
|
||||
QUEUE_EMAIL_BOX_HOST = getattr(settings, 'QUEUE_EMAIL_BOX_HOST', None)
|
||||
QUEUE_EMAIL_BOX_USER = getattr(settings, 'QUEUE_EMAIL_BOX_USER', None)
|
||||
QUEUE_EMAIL_BOX_PASSWORD = getattr(settings, 'QUEUE_EMAIL_BOX_PASSWORD', None)
|
||||
|
||||
# only process emails with a valid tracking ID? (throws away all other mail)
|
||||
QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr(settings, 'QUEUE_EMAIL_BOX_UPDATE_ONLY', False)
|
||||
|
||||
# only allow users to access queues that they are members of?
|
||||
HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = getattr(
|
||||
settings, 'HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION', False)
|
434
build/lib/helpdesk/static/helpdesk/dist/css/sb-admin-2.css
vendored
Normal file
434
build/lib/helpdesk/static/helpdesk/dist/css/sb-admin-2.css
vendored
Normal file
@ -0,0 +1,434 @@
|
||||
/*!
|
||||
* Start Bootstrap - SB Admin 2 v3.3.7+1 (http://startbootstrap.com/template-overviews/sb-admin-2)
|
||||
* Copyright 2013-2016 Start Bootstrap
|
||||
* Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap/blob/gh-pages/LICENSE)
|
||||
*/
|
||||
body {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
#wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
#page-wrapper {
|
||||
padding: 0 15px;
|
||||
min-height: 568px;
|
||||
background-color: white;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
#page-wrapper {
|
||||
position: inherit;
|
||||
margin: 0 0 0 250px;
|
||||
padding: 0 30px;
|
||||
border-left: 1px solid #e7e7e7;
|
||||
}
|
||||
}
|
||||
.navbar-top-links {
|
||||
margin-right: 0;
|
||||
}
|
||||
.navbar-top-links li {
|
||||
display: inline-block;
|
||||
}
|
||||
.navbar-top-links li:last-child {
|
||||
margin-right: 15px;
|
||||
}
|
||||
.navbar-top-links li a {
|
||||
padding: 15px;
|
||||
min-height: 50px;
|
||||
}
|
||||
.navbar-top-links .dropdown-menu li {
|
||||
display: block;
|
||||
}
|
||||
.navbar-top-links .dropdown-menu li:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
.navbar-top-links .dropdown-menu li a {
|
||||
padding: 3px 20px;
|
||||
min-height: 0;
|
||||
}
|
||||
.navbar-top-links .dropdown-menu li a div {
|
||||
white-space: normal;
|
||||
}
|
||||
.navbar-top-links .dropdown-messages,
|
||||
.navbar-top-links .dropdown-tasks,
|
||||
.navbar-top-links .dropdown-alerts {
|
||||
width: 310px;
|
||||
min-width: 0;
|
||||
}
|
||||
.navbar-top-links .dropdown-messages {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.navbar-top-links .dropdown-tasks {
|
||||
margin-left: -59px;
|
||||
}
|
||||
.navbar-top-links .dropdown-alerts {
|
||||
margin-left: -123px;
|
||||
}
|
||||
.navbar-top-links .dropdown-user {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
.sidebar .sidebar-nav.navbar-collapse {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
.sidebar .sidebar-search {
|
||||
padding: 15px;
|
||||
}
|
||||
.sidebar ul li {
|
||||
border-bottom: 1px solid #e7e7e7;
|
||||
}
|
||||
.sidebar ul li a.active {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
.sidebar .arrow {
|
||||
float: right;
|
||||
}
|
||||
.sidebar .fa.arrow:before {
|
||||
content: "\f104";
|
||||
}
|
||||
.sidebar .active > a > .fa.arrow:before {
|
||||
content: "\f107";
|
||||
}
|
||||
.sidebar .nav-second-level li,
|
||||
.sidebar .nav-third-level li {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
.sidebar .nav-second-level li a {
|
||||
padding-left: 37px;
|
||||
}
|
||||
.sidebar .nav-third-level li a {
|
||||
padding-left: 52px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.sidebar {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
width: 250px;
|
||||
margin-top: 51px;
|
||||
}
|
||||
.navbar-top-links .dropdown-messages,
|
||||
.navbar-top-links .dropdown-tasks,
|
||||
.navbar-top-links .dropdown-alerts {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
.btn-outline {
|
||||
color: inherit;
|
||||
background-color: transparent;
|
||||
transition: all .5s;
|
||||
}
|
||||
.btn-primary.btn-outline {
|
||||
color: #428bca;
|
||||
}
|
||||
.btn-success.btn-outline {
|
||||
color: #5cb85c;
|
||||
}
|
||||
.btn-info.btn-outline {
|
||||
color: #5bc0de;
|
||||
}
|
||||
.btn-warning.btn-outline {
|
||||
color: #f0ad4e;
|
||||
}
|
||||
.btn-danger.btn-outline {
|
||||
color: #d9534f;
|
||||
}
|
||||
.btn-primary.btn-outline:hover,
|
||||
.btn-success.btn-outline:hover,
|
||||
.btn-info.btn-outline:hover,
|
||||
.btn-warning.btn-outline:hover,
|
||||
.btn-danger.btn-outline:hover {
|
||||
color: white;
|
||||
}
|
||||
.chat {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.chat li {
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px dotted #999999;
|
||||
}
|
||||
.chat li.left .chat-body {
|
||||
margin-left: 60px;
|
||||
}
|
||||
.chat li.right .chat-body {
|
||||
margin-right: 60px;
|
||||
}
|
||||
.chat li .chat-body p {
|
||||
margin: 0;
|
||||
}
|
||||
.panel .slidedown .glyphicon,
|
||||
.chat .glyphicon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.chat-panel .panel-body {
|
||||
height: 350px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.login-panel {
|
||||
margin-top: 25%;
|
||||
}
|
||||
.flot-chart {
|
||||
display: block;
|
||||
height: 400px;
|
||||
}
|
||||
.flot-chart-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
table.dataTable thead .sorting,
|
||||
table.dataTable thead .sorting_asc,
|
||||
table.dataTable thead .sorting_desc,
|
||||
table.dataTable thead .sorting_asc_disabled,
|
||||
table.dataTable thead .sorting_desc_disabled {
|
||||
background: transparent;
|
||||
}
|
||||
table.dataTable thead .sorting_asc:after {
|
||||
content: "\f0de";
|
||||
float: right;
|
||||
font-family: fontawesome;
|
||||
}
|
||||
table.dataTable thead .sorting_desc:after {
|
||||
content: "\f0dd";
|
||||
float: right;
|
||||
font-family: fontawesome;
|
||||
}
|
||||
table.dataTable thead .sorting:after {
|
||||
content: "\f0dc";
|
||||
float: right;
|
||||
font-family: fontawesome;
|
||||
color: rgba(50, 50, 50, 0.5);
|
||||
}
|
||||
.btn-circle {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 6px 0;
|
||||
border-radius: 15px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
line-height: 1.428571429;
|
||||
}
|
||||
.btn-circle.btn-lg {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 25px;
|
||||
font-size: 18px;
|
||||
line-height: 1.33;
|
||||
}
|
||||
.btn-circle.btn-xl {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 35px;
|
||||
font-size: 24px;
|
||||
line-height: 1.33;
|
||||
}
|
||||
.show-grid [class^="col-"] {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #eee !important;
|
||||
}
|
||||
.show-grid {
|
||||
margin: 15px 0;
|
||||
}
|
||||
.huge {
|
||||
font-size: 40px;
|
||||
}
|
||||
.panel-green {
|
||||
border-color: #5cb85c;
|
||||
}
|
||||
.panel-green > .panel-heading {
|
||||
border-color: #5cb85c;
|
||||
color: white;
|
||||
background-color: #5cb85c;
|
||||
}
|
||||
.panel-green > a {
|
||||
color: #5cb85c;
|
||||
}
|
||||
.panel-green > a:hover {
|
||||
color: #3d8b3d;
|
||||
}
|
||||
.panel-red {
|
||||
border-color: #d9534f;
|
||||
}
|
||||
.panel-red > .panel-heading {
|
||||
border-color: #d9534f;
|
||||
color: white;
|
||||
background-color: #d9534f;
|
||||
}
|
||||
.panel-red > a {
|
||||
color: #d9534f;
|
||||
}
|
||||
.panel-red > a:hover {
|
||||
color: #b52b27;
|
||||
}
|
||||
.panel-yellow {
|
||||
border-color: #f0ad4e;
|
||||
}
|
||||
.panel-yellow > .panel-heading {
|
||||
border-color: #f0ad4e;
|
||||
color: white;
|
||||
background-color: #f0ad4e;
|
||||
}
|
||||
.panel-yellow > a {
|
||||
color: #f0ad4e;
|
||||
}
|
||||
.panel-yellow > a:hover {
|
||||
color: #df8a13;
|
||||
}
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding: 20px 0 20px;
|
||||
list-style: none;
|
||||
}
|
||||
.timeline:before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 3px;
|
||||
margin-left: -1.5px;
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
.timeline > li {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.timeline > li:before,
|
||||
.timeline > li:after {
|
||||
content: " ";
|
||||
display: table;
|
||||
}
|
||||
.timeline > li:after {
|
||||
clear: both;
|
||||
}
|
||||
.timeline > li:before,
|
||||
.timeline > li:after {
|
||||
content: " ";
|
||||
display: table;
|
||||
}
|
||||
.timeline > li:after {
|
||||
clear: both;
|
||||
}
|
||||
.timeline > li > .timeline-panel {
|
||||
float: left;
|
||||
position: relative;
|
||||
width: 46%;
|
||||
padding: 20px;
|
||||
border: 1px solid #d4d4d4;
|
||||
border-radius: 2px;
|
||||
-webkit-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.175);
|
||||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.175);
|
||||
}
|
||||
.timeline > li > .timeline-panel:before {
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 26px;
|
||||
right: -15px;
|
||||
border-top: 15px solid transparent;
|
||||
border-right: 0 solid #ccc;
|
||||
border-bottom: 15px solid transparent;
|
||||
border-left: 15px solid #ccc;
|
||||
}
|
||||
.timeline > li > .timeline-panel:after {
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 27px;
|
||||
right: -14px;
|
||||
border-top: 14px solid transparent;
|
||||
border-right: 0 solid #fff;
|
||||
border-bottom: 14px solid transparent;
|
||||
border-left: 14px solid #fff;
|
||||
}
|
||||
.timeline > li > .timeline-badge {
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin-left: -25px;
|
||||
border-radius: 50% 50% 50% 50%;
|
||||
text-align: center;
|
||||
font-size: 1.4em;
|
||||
line-height: 50px;
|
||||
color: #fff;
|
||||
background-color: #999999;
|
||||
}
|
||||
.timeline > li.timeline-inverted > .timeline-panel {
|
||||
float: right;
|
||||
}
|
||||
.timeline > li.timeline-inverted > .timeline-panel:before {
|
||||
right: auto;
|
||||
left: -15px;
|
||||
border-right-width: 15px;
|
||||
border-left-width: 0;
|
||||
}
|
||||
.timeline > li.timeline-inverted > .timeline-panel:after {
|
||||
right: auto;
|
||||
left: -14px;
|
||||
border-right-width: 14px;
|
||||
border-left-width: 0;
|
||||
}
|
||||
.timeline-badge.primary {
|
||||
background-color: #2e6da4 !important;
|
||||
}
|
||||
.timeline-badge.success {
|
||||
background-color: #3f903f !important;
|
||||
}
|
||||
.timeline-badge.warning {
|
||||
background-color: #f0ad4e !important;
|
||||
}
|
||||
.timeline-badge.danger {
|
||||
background-color: #d9534f !important;
|
||||
}
|
||||
.timeline-badge.info {
|
||||
background-color: #5bc0de !important;
|
||||
}
|
||||
.timeline-title {
|
||||
margin-top: 0;
|
||||
color: inherit;
|
||||
}
|
||||
.timeline-body > p,
|
||||
.timeline-body > ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.timeline-body > p + p {
|
||||
margin-top: 5px;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
ul.timeline:before {
|
||||
left: 40px;
|
||||
}
|
||||
ul.timeline > li > .timeline-panel {
|
||||
width: calc(10%);
|
||||
width: -moz-calc(10%);
|
||||
width: -webkit-calc(10%);
|
||||
}
|
||||
ul.timeline > li > .timeline-badge {
|
||||
top: 16px;
|
||||
left: 15px;
|
||||
margin-left: 0;
|
||||
}
|
||||
ul.timeline > li > .timeline-panel {
|
||||
float: right;
|
||||
}
|
||||
ul.timeline > li > .timeline-panel:before {
|
||||
right: auto;
|
||||
left: -15px;
|
||||
border-right-width: 15px;
|
||||
border-left-width: 0;
|
||||
}
|
||||
ul.timeline > li > .timeline-panel:after {
|
||||
right: auto;
|
||||
left: -14px;
|
||||
border-right-width: 14px;
|
||||
border-left-width: 0;
|
||||
}
|
||||
}
|
5
build/lib/helpdesk/static/helpdesk/dist/css/sb-admin-2.min.css
vendored
Normal file
5
build/lib/helpdesk/static/helpdesk/dist/css/sb-admin-2.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
47
build/lib/helpdesk/static/helpdesk/dist/js/sb-admin-2.js
vendored
Normal file
47
build/lib/helpdesk/static/helpdesk/dist/js/sb-admin-2.js
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
/*!
|
||||
* Start Bootstrap - SB Admin 2 v3.3.7+1 (http://startbootstrap.com/template-overviews/sb-admin-2)
|
||||
* Copyright 2013-2016 Start Bootstrap
|
||||
* Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap/blob/gh-pages/LICENSE)
|
||||
*/
|
||||
$(function() {
|
||||
$('#side-menu').metisMenu();
|
||||
});
|
||||
|
||||
//Loads the correct sidebar on window load,
|
||||
//collapses the sidebar on window resize.
|
||||
// Sets the min-height of #page-wrapper to window size
|
||||
$(function() {
|
||||
$(window).bind("load resize", function() {
|
||||
var topOffset = 50;
|
||||
var width = (this.window.innerWidth > 0) ? this.window.innerWidth : this.screen.width;
|
||||
if (width < 768) {
|
||||
$('div.navbar-collapse').addClass('collapse');
|
||||
topOffset = 100; // 2-row-menu
|
||||
} else {
|
||||
$('div.navbar-collapse').removeClass('collapse');
|
||||
}
|
||||
|
||||
var height = ((this.window.innerHeight > 0) ? this.window.innerHeight : this.screen.height) - 1;
|
||||
height = height - topOffset;
|
||||
if (height < 1) height = 1;
|
||||
if (height > topOffset) {
|
||||
$("#page-wrapper").css("min-height", (height) + "px");
|
||||
}
|
||||
});
|
||||
|
||||
var url = window.location;
|
||||
// var element = $('ul.nav a').filter(function() {
|
||||
// return this.href == url;
|
||||
// }).addClass('active').parent().parent().addClass('in').parent();
|
||||
var element = $('ul.nav a').filter(function() {
|
||||
return this.href == url;
|
||||
}).addClass('active').parent();
|
||||
|
||||
while (true) {
|
||||
if (element.is('li')) {
|
||||
element = element.parent().addClass('in').parent();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
6
build/lib/helpdesk/static/helpdesk/dist/js/sb-admin-2.min.js
vendored
Normal file
6
build/lib/helpdesk/static/helpdesk/dist/js/sb-admin-2.min.js
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* Start Bootstrap - SB Admin 2 v3.3.7+1 (http://startbootstrap.com/template-overviews/sb-admin-2)
|
||||
* Copyright 2013-2016 Start Bootstrap
|
||||
* Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap/blob/gh-pages/LICENSE)
|
||||
*/
|
||||
$(function(){$("#side-menu").metisMenu()}),$(function(){$(window).bind("load resize",function(){var i=50,n=this.window.innerWidth>0?this.window.innerWidth:this.screen.width;n<768?($("div.navbar-collapse").addClass("collapse"),i=100):$("div.navbar-collapse").removeClass("collapse");var e=(this.window.innerHeight>0?this.window.innerHeight:this.screen.height)-1;e-=i,e<1&&(e=1),e>i&&$("#page-wrapper").css("min-height",e+"px")});for(var i=window.location,n=$("ul.nav a").filter(function(){return this.href==i}).addClass("active").parent();;){if(!n.is("li"))break;n=n.parent().addClass("in").parent()}});
|
21
build/lib/helpdesk/static/helpdesk/filter.js
Normal file
21
build/lib/helpdesk/static/helpdesk/filter.js
Normal file
@ -0,0 +1,21 @@
|
||||
$(document).ready(function() {
|
||||
$("#filterBuilderButton").click(function() {
|
||||
var boxName = "#filterBox" + $("#filterBuilderSelect").val();
|
||||
$(boxName).slideDown();
|
||||
return false;
|
||||
});
|
||||
$(".filterBuilderRemove").click(function() {
|
||||
var boxName = "#" + $(this).parents(".filterBox").attr('id');
|
||||
$(boxName).slideUp();
|
||||
$(boxName).children("input:text").each(function() {
|
||||
$(this).val("");
|
||||
});
|
||||
$(boxName).children("input:checkbox").each(function() {
|
||||
this.checked = false;
|
||||
});
|
||||
$(boxName).children("select").each(function() {
|
||||
this.selectedIndex = -1;
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
78
build/lib/helpdesk/static/helpdesk/helpdesk-extend.css
Normal file
78
build/lib/helpdesk/static/helpdesk/helpdesk-extend.css
Normal file
@ -0,0 +1,78 @@
|
||||
/*
|
||||
Bootstrap overrides
|
||||
*/
|
||||
|
||||
.btn-file {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.btn-file input[type=file] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
font-size: 100px;
|
||||
text-align: right;
|
||||
filter: alpha(opacity=0);
|
||||
opacity: 0;
|
||||
outline: none;
|
||||
background: white;
|
||||
cursor: inherit;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumbnail.filterBox {
|
||||
display: none;
|
||||
float: left;
|
||||
border: solid #ccc 1px;
|
||||
padding: 10px;
|
||||
margin: 4px;
|
||||
max-width: 24%;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.thumbnail.filterBoxShow {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.filterBox label {
|
||||
clear: both;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.filterBox .filterHelp {
|
||||
color: #aaa;
|
||||
font-size: 0.8em;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#searchtabs {margin-bottom: 20px;}
|
||||
|
||||
.row_tablehead, table.table caption {background-color: #dbd5d9;}
|
||||
table.table caption {
|
||||
padding-left: 2em;
|
||||
line-height: 2em; font-weight: bold;
|
||||
}
|
||||
table.ticket-stats caption {color: #fbff00; font-style: italic;}
|
||||
table.ticket-stats tbody th, table.ticket-stats tbody tr {padding-left: 20px}
|
||||
|
||||
.errorlist {list-style: none;}
|
||||
.errorlist {padding: 0;}
|
||||
.has-error .input-group input, .has-error .input-group select, .has-error .input-group textarea {border-color: #b94a48}
|
||||
|
||||
#helpdesk-nav-collapse #searchform {
|
||||
padding-top: 0;
|
||||
}
|
||||
#ticket-description {background-color: #FCF8E3;}
|
||||
.followup.well {background-color: #f4f5ff;}
|
||||
/*
|
||||
Add your custom styles here
|
||||
*/
|
||||
#footer {
|
||||
border-top: 2px solid #AAAAAA;
|
||||
margin-top: 20px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
#helpdesk-body {padding-top: 100px;}
|
||||
img.brand {padding-right: 30px;}
|
323
build/lib/helpdesk/static/helpdesk/helpdesk.css
Normal file
323
build/lib/helpdesk/static/helpdesk/helpdesk.css
Normal file
@ -0,0 +1,323 @@
|
||||
body {
|
||||
font: 10pt "Trebuchet MS", Arial, sans-serif;
|
||||
background-color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin-top: 8px;
|
||||
padding: 80px;
|
||||
}
|
||||
|
||||
#container {
|
||||
width: 700px;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#header h1 {
|
||||
float: left;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
#header ul {
|
||||
float: right;
|
||||
margin-top: 40px;
|
||||
line-height: 24px;
|
||||
}
|
||||
#header li {
|
||||
display: inline;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#header li a {
|
||||
padding: 2px 3px;
|
||||
margin: 2px;
|
||||
border: solid #444 1px;
|
||||
color: #000;
|
||||
background-color: #eee;
|
||||
text-decoration: none;
|
||||
font-size: 10pt;
|
||||
line-height: 12pt;
|
||||
}
|
||||
|
||||
#searchform .input {
|
||||
border: solid #444 1px;
|
||||
background-color: #fff;
|
||||
color: #ccc;
|
||||
padding: 0px 3px;
|
||||
margin: 0px 2px;
|
||||
font-size: 10pt;
|
||||
line-height: 12pt;
|
||||
}
|
||||
|
||||
#searchform .input:focus {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#header li a:hover {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
#header, #body, #footer {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
span.form_optional {
|
||||
color: #666;
|
||||
font-size: 95%;
|
||||
}
|
||||
|
||||
dd.form_help_text {
|
||||
color: #666;
|
||||
font-size: 95%;
|
||||
}
|
||||
|
||||
ul.errorlist {
|
||||
color: #a33;
|
||||
font-size: 95%;
|
||||
}
|
||||
|
||||
dt {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.row_tablehead {
|
||||
background-color: #6593C0;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
border-bottom: solid white 1px;
|
||||
}
|
||||
|
||||
.row_tablehead span.ticket_toolbar {
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.row_tablehead td {
|
||||
padding-left: 12px;
|
||||
line-height: 16pt;
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
.row_columnheads {
|
||||
background-color: #94C0E8;
|
||||
font-size: 10pt;
|
||||
line-height: 12pt;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
color: #3E5F84;
|
||||
text-align: left;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.row_odd {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.row_even {
|
||||
background-color: #ecf6fc;
|
||||
}
|
||||
|
||||
.row_odd, .row_even {
|
||||
color: #6C79A0;
|
||||
border-bottom: solid #d5e7fd 1px;
|
||||
}
|
||||
|
||||
.row_odd th, .row_even th {
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
.row_odd td:first-child, .row_even td:first-child {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.cell_bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
td a, th a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
td {
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
td.report {
|
||||
font-size: 10pt;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hover {
|
||||
background-color: #bcd4ec;
|
||||
}
|
||||
|
||||
div.followup {
|
||||
width: 100%;
|
||||
border-top: solid #666 1px;
|
||||
padding:0 0 2px;
|
||||
}
|
||||
|
||||
div.followup_mod {
|
||||
width: auto;
|
||||
border: solid #666 1px;
|
||||
padding: 5px;
|
||||
margin: 0px 0px 10px;
|
||||
}
|
||||
|
||||
div.followup .title, div.followup_mod .title {
|
||||
font-weight: bold;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
div.followup .title span, div.followup_mod .title span {
|
||||
color: #aaa;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
div.followup_mod small {
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
span.private {
|
||||
color: #aaa;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
a.ticket_link_status_Closed {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
a.ticket_link_status_Open {
|
||||
color: #393;
|
||||
}
|
||||
|
||||
a.ticket_link_status_Reopened {
|
||||
color: #393;
|
||||
}
|
||||
|
||||
a.ticket_link_status_Resolved {
|
||||
color: #996;
|
||||
}
|
||||
|
||||
a.ticket_link_status {
|
||||
color: #369;
|
||||
font: 12pt Garamond;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
textarea#commentBox {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filterBox {
|
||||
display: none;
|
||||
float: left;
|
||||
border: solid #ccc 1px;
|
||||
padding: 2px;
|
||||
margin: 4px;
|
||||
max-width: 24%;
|
||||
}
|
||||
|
||||
.filterBoxShow {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.filterBox label {
|
||||
clear: both;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.filterBox .filterHelp {
|
||||
color: #aaa;
|
||||
font-size: 0.8em;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
span.priority1, span.priority2, span.priority3, span.priority4, span.priority5 {
|
||||
padding: 1px 12px;
|
||||
font-weight: bold;
|
||||
-moz-border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
margin: 2px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
span.priority1 {
|
||||
background-color: #c00;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
span.priority2 {
|
||||
background-color: #e80;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
span.priority3 {
|
||||
background-color: #fe8;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
span.priority4 {
|
||||
background-color: #59c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
span.priority5 {
|
||||
background-color: #8c5;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
a.followup-edit {
|
||||
float:right;
|
||||
}
|
||||
|
||||
/* tooltips for link hover */
|
||||
a.tooltip, a.tooltip:link, a.tooltip:visited, a.tooltip:active {
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
a.tooltip:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
a.tooltip span {
|
||||
display: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.tooltip:hover span {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
left: 0;
|
||||
width: 250px;
|
||||
z-index: 10;
|
||||
color: #000000;
|
||||
border:1px solid #000000;
|
||||
background: #FFFFCC;
|
||||
font: 12px Verdana, sans-serif;
|
||||
text-align: left;
|
||||
}
|
||||
|
1546
build/lib/helpdesk/static/helpdesk/jquery.translate-debug-all.js
Normal file
1546
build/lib/helpdesk/static/helpdesk/jquery.translate-debug-all.js
Normal file
File diff suppressed because it is too large
Load Diff
42
build/lib/helpdesk/static/helpdesk/js/sb-admin-2.js
Normal file
42
build/lib/helpdesk/static/helpdesk/js/sb-admin-2.js
Normal file
@ -0,0 +1,42 @@
|
||||
$(function() {
|
||||
$('#side-menu').metisMenu();
|
||||
});
|
||||
|
||||
//Loads the correct sidebar on window load,
|
||||
//collapses the sidebar on window resize.
|
||||
// Sets the min-height of #page-wrapper to window size
|
||||
$(function() {
|
||||
$(window).bind("load resize", function() {
|
||||
var topOffset = 50;
|
||||
var width = (this.window.innerWidth > 0) ? this.window.innerWidth : this.screen.width;
|
||||
if (width < 768) {
|
||||
$('div.navbar-collapse').addClass('collapse');
|
||||
topOffset = 100; // 2-row-menu
|
||||
} else {
|
||||
$('div.navbar-collapse').removeClass('collapse');
|
||||
}
|
||||
|
||||
var height = ((this.window.innerHeight > 0) ? this.window.innerHeight : this.screen.height) - 1;
|
||||
height = height - topOffset;
|
||||
if (height < 1) height = 1;
|
||||
if (height > topOffset) {
|
||||
$("#page-wrapper").css("min-height", (height) + "px");
|
||||
}
|
||||
});
|
||||
|
||||
var url = window.location;
|
||||
// var element = $('ul.nav a').filter(function() {
|
||||
// return this.href == url;
|
||||
// }).addClass('active').parent().parent().addClass('in').parent();
|
||||
var element = $('ul.nav a').filter(function() {
|
||||
return this.href == url;
|
||||
}).addClass('active').parent();
|
||||
|
||||
while (true) {
|
||||
if (element.is('li')) {
|
||||
element = element.parent().addClass('in').parent();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user