mirror of
https://github.com/django-helpdesk/django-helpdesk.git
synced 2024-11-07 08:44:04 +01:00
Fixing style (according to branch 'autocodestyle') + quicktest
This commit is contained in:
commit
89a6ae01b8
25
.coveragerc
Normal file
25
.coveragerc
Normal file
@ -0,0 +1,25 @@
|
||||
# .coveragerc to control coverage.py
|
||||
# following the example at http://nedbatchelder.com/code/coverage/config.html
|
||||
[run]
|
||||
branch = True
|
||||
include = helpdesk/*
|
||||
omit =
|
||||
*helpdesk/south_migrations/*
|
||||
*helpdesk/migrations/*
|
||||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
exclude_lines =
|
||||
# Have to re-enable the standard pragma
|
||||
pragma: no cover
|
||||
|
||||
# Don't complain if tests don't hit defensive assertion code:
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
|
||||
# Don't complain if non-runnable code isn't run:
|
||||
if 0:
|
||||
if __name__ == .__main__.:
|
||||
if __name__==.__main__.:
|
||||
|
||||
ignore_errors = True
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,5 +3,6 @@ dist
|
||||
django_helpdesk.egg-info
|
||||
docs/html/*
|
||||
docs/doctrees/*
|
||||
.coverage
|
||||
.project
|
||||
.pydevproject
|
378
.pylintrc
Normal file
378
.pylintrc
Normal file
@ -0,0 +1,378 @@
|
||||
[MASTER]
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS,migrations,south_migrations
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=pylint_django
|
||||
|
||||
# Use multiple processes to speed up Pylint.
|
||||
jobs=1
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Allow optimization of some AST trees. This will activate a peephole AST
|
||||
# optimizer, which will apply various small optimizations. For instance, it can
|
||||
# be used to obtain the result of joining multiple strings with the addition
|
||||
# operator. Joining a lot of strings can lead to a maximum recursion error in
|
||||
# Pylint and this flag can prevent that. It has one side effect, the resulting
|
||||
# AST will be different than the one from reality.
|
||||
optimize-ast=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
|
||||
confidence=
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time. See also the "--disable" option for examples.
|
||||
#enable=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once).You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||
# --disable=W"
|
||||
disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, msvs
|
||||
# (visual studio) and html. You can also give a reporter class, eg
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Put messages in a separate file for each module / package specified on the
|
||||
# command line instead of printing them on stdout. Reports (if any) will be
|
||||
# written in a file name "pylint_global.[txt|html]".
|
||||
files-output=no
|
||||
|
||||
# Tells whether to display a full report or only the messages
|
||||
reports=yes
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details
|
||||
#msg-template=
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=120
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
# List of optional constructs for which whitespace checking is disabled. `dict-
|
||||
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
|
||||
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
|
||||
# `empty-line` allows space-only lines.
|
||||
no-space-check=trailing-comma,dict-separator
|
||||
|
||||
# Maximum number of lines in a module
|
||||
max-module-lines=1000
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||
# install python-enchant package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in
|
||||
# --spelling-private-dict-file option instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expectedly
|
||||
# not used).
|
||||
dummy-variables-rgx=_$|dummy
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid to define new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,_cb
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,XXX,TODO
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# List of classes names for which member attributes should not be checked
|
||||
# (useful for classes with attributes dynamically set). This supports can work
|
||||
# with qualified names.
|
||||
ignored-classes=
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# List of builtins function names that should not be used, separated by a comma
|
||||
bad-functions=map,filter,input
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma
|
||||
good-names=i,j,k,ex,Run,_
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma
|
||||
bad-names=foo,bar,baz,toto,tutu,tata
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name
|
||||
include-naming-hint=no
|
||||
|
||||
# Regular expression matching correct function names
|
||||
function-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for function names
|
||||
function-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct variable names
|
||||
variable-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for variable names
|
||||
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct constant names
|
||||
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||
|
||||
# Naming hint for constant names
|
||||
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||
|
||||
# Regular expression matching correct attribute names
|
||||
attr-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for attribute names
|
||||
attr-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct argument names
|
||||
argument-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for argument names
|
||||
argument-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct class attribute names
|
||||
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
||||
|
||||
# Naming hint for class attribute names
|
||||
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
||||
|
||||
# Regular expression matching correct inline iteration names
|
||||
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
|
||||
|
||||
# Naming hint for inline iteration names
|
||||
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
|
||||
|
||||
# Regular expression matching correct class names
|
||||
class-rgx=[A-Z_][a-zA-Z0-9]+$
|
||||
|
||||
# Naming hint for class names
|
||||
class-name-hint=[A-Z_][a-zA-Z0-9]+$
|
||||
|
||||
# Regular expression matching correct module names
|
||||
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||
|
||||
# Naming hint for module names
|
||||
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||
|
||||
# Regular expression matching correct method names
|
||||
method-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for method names
|
||||
method-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
|
||||
[ELIF]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method
|
||||
max-args=5
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore
|
||||
ignored-argument-names=_.*
|
||||
|
||||
# Maximum number of locals for function / method body
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of return / yield for function / method body
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of branch for function / method body
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of statements in function / method body
|
||||
max-statements=50
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of boolean expressions in a if statement
|
||||
max-bool-expr=5
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma
|
||||
deprecated-modules=regsub,TERMIOS,Bastion,rexec
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled)
|
||||
import-graph=
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
int-import-graph=
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,__new__,setUp
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,_fields,_replace,_source,_make
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception"
|
||||
overgeneral-exceptions=Exception
|
@ -9,6 +9,7 @@ env:
|
||||
- DJANGO=1.7.11
|
||||
- DJANGO=1.8.7
|
||||
- DJANGO=1.9
|
||||
- DJANGO=1.10
|
||||
|
||||
matrix:
|
||||
exclude:
|
||||
@ -17,7 +18,13 @@ matrix:
|
||||
|
||||
install:
|
||||
- pip install argparse
|
||||
- pip install coverage
|
||||
- pip install codecov
|
||||
- pip install -q Django==$DJANGO
|
||||
- pip install -q -r requirements.txt
|
||||
|
||||
script: python quicktest.py helpdesk
|
||||
script:
|
||||
- coverage run --source='.' quicktest.py helpdesk
|
||||
|
||||
after_success:
|
||||
- codecov
|
10
README.rst
10
README.rst
@ -4,6 +4,9 @@ django-helpdesk - A Django powered ticket tracker for small businesses.
|
||||
.. image:: https://travis-ci.org/django-helpdesk/django-helpdesk.png?branch=master
|
||||
:target: https://travis-ci.org/django-helpdesk/django-helpdesk
|
||||
|
||||
.. image:: https://codecov.io/gh/django-helpdesk/django-helpdesk/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/django-helpdesk/django-helpdesk
|
||||
|
||||
Copyright 2009- Ross Poulton and contributors. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
django-helpdesk was formerly known as Jutda Helpdesk, named after the
|
||||
@ -26,7 +29,7 @@ Dependencies (pre-flight checklist)
|
||||
-----------------------------------
|
||||
|
||||
1. Python 2.7 or 3.4+ (3.4+ support is new, please let us know how it goes)
|
||||
2. Django (1.7 or newer, preferably 1.9 - Django 1.7 is not supported if you are using Python 3.5)
|
||||
2. Django (1.7, 1.8, 1.9 and 1.10, preferably 1.9 - Django 1.7 is not supported if you are using Python 3.5)
|
||||
3. An existing WORKING Django project with database etc. If you
|
||||
cannot log into the Admin, you won't get this product working.
|
||||
4. `pip install django-bootstrap-form` and add `bootstrapform` to `settings.INSTALLED_APPS`
|
||||
@ -98,7 +101,4 @@ https://www.transifex.com/django-helpdesk/django-helpdesk/
|
||||
|
||||
Feel free to request access to contribute your translations.
|
||||
|
||||
Pull requests for all other changes are welcome. We're currently trying to add test cases wherever possible, so please continue to include tests with pull requests.
|
||||
|
||||
.. image:: https://secure.travis-ci.org/django-helpdesk/django-helpdesk.png?branch=master
|
||||
:target: https://travis-ci.org/django-helpdesk/django-helpdesk
|
||||
Pull requests for all other changes are welcome. We're currently trying to add test cases wherever possible, so please continue to include tests with pull requests.
|
@ -1,7 +1,7 @@
|
||||
Settings
|
||||
========
|
||||
|
||||
First, django-helpdesk needs ``django.core.context_processors.request`` activated, so in your ``settings.py`` add::
|
||||
First, django-helpdesk needs ``django.core.context_processors.request`` activated, so you must add it to the ``settings.py``. For Django 1.7, add::
|
||||
|
||||
from django.conf import global_settings
|
||||
TEMPLATE_CONTEXT_PROCESSORS = (
|
||||
@ -9,6 +9,25 @@ First, django-helpdesk needs ``django.core.context_processors.request`` activat
|
||||
('django.core.context_processors.request',)
|
||||
)
|
||||
|
||||
For Django 1.8 and onwards, the settings are located in the ``TEMPLATES``, and the ``request`` module has moved. Add the following instead::
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
...
|
||||
'OPTIONS': {
|
||||
...
|
||||
'context_processors': (
|
||||
# Default ones first
|
||||
...
|
||||
# The one django-helpdesk requires:
|
||||
"django.template.context_processors.request",
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
The following settings can be changed in your ``settings.py`` file to help change the way django-helpdesk operates. There are quite a few settings available to toggle functionality within django-helpdesk.
|
||||
|
||||
HELPDESK_DEFAULT_SETTINGS
|
||||
|
@ -5,10 +5,14 @@ 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'
|
||||
@ -24,34 +28,38 @@ class TicketAdmin(admin.ModelAdmin):
|
||||
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]
|
||||
|
||||
|
||||
@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.site.register(Ticket, TicketAdmin)
|
||||
admin.site.register(Queue, QueueAdmin)
|
||||
admin.site.register(FollowUp, FollowUpAdmin)
|
||||
admin.site.register(PreSetReply)
|
||||
admin.site.register(EscalationExclusion)
|
||||
admin.site.register(EmailTemplate, EmailTemplateAdmin)
|
||||
admin.site.register(KBCategory)
|
||||
admin.site.register(KBItem, KBItemAdmin)
|
||||
admin.site.register(IgnoreEmail)
|
||||
admin.site.register(CustomField, CustomFieldAdmin)
|
||||
|
@ -55,7 +55,7 @@ Usage example::
|
||||
"""
|
||||
|
||||
|
||||
import os, sys
|
||||
import os
|
||||
from urllib import urlencode
|
||||
|
||||
import socket
|
||||
@ -104,9 +104,13 @@ else:
|
||||
|
||||
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"""
|
||||
@ -120,7 +124,6 @@ class Akismet(object):
|
||||
self.user_agent = user_agent % (agent, __version__)
|
||||
self.setAPIKey(key, blog_url)
|
||||
|
||||
|
||||
def _getURL(self):
|
||||
"""
|
||||
Fetch the url to make requests to.
|
||||
@ -128,8 +131,7 @@ class Akismet(object):
|
||||
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)
|
||||
@ -137,7 +139,6 @@ class Akismet(object):
|
||||
raise AkismetError(str(e))
|
||||
return resp
|
||||
|
||||
|
||||
def setAPIKey(self, key=None, blog_url=None):
|
||||
"""
|
||||
Set the wordpress API key for all transactions.
|
||||
@ -151,7 +152,7 @@ class Akismet(object):
|
||||
"""
|
||||
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('#')]
|
||||
if l.strip() and not l.strip().startswith('#')]
|
||||
try:
|
||||
self.key = the_file[0]
|
||||
self.blog_url = the_file[1]
|
||||
@ -161,7 +162,6 @@ class Akismet(object):
|
||||
self.key = key
|
||||
self.blog_url = blog_url
|
||||
|
||||
|
||||
def verify_key(self):
|
||||
"""
|
||||
This equates to the ``verify-key`` call against the akismet API.
|
||||
@ -179,12 +179,12 @@ class Akismet(object):
|
||||
"""
|
||||
if self.key is None:
|
||||
raise APIKeyError("Your have not set an API key.")
|
||||
data = { 'key': self.key, 'blog': self.blog_url }
|
||||
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}
|
||||
headers = {'User-Agent': self.user_agent}
|
||||
resp = self._safeRequest(url, urlencode(data), headers)
|
||||
if resp.lower() == 'valid':
|
||||
return True
|
||||
@ -226,14 +226,11 @@ class Akismet(object):
|
||||
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('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.
|
||||
@ -316,7 +313,7 @@ class Akismet(object):
|
||||
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}
|
||||
headers = {'User-Agent': self.user_agent}
|
||||
resp = self._safeRequest(url, urlencode(data), headers)
|
||||
if DEBUG:
|
||||
return resp
|
||||
@ -329,7 +326,6 @@ class Akismet(object):
|
||||
# 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,
|
||||
@ -347,10 +343,9 @@ class Akismet(object):
|
||||
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}
|
||||
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,
|
||||
@ -368,5 +363,5 @@ class Akismet(object):
|
||||
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}
|
||||
headers = {'User-Agent': self.user_agent}
|
||||
self._safeRequest(url, urlencode(data), headers)
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class HelpdeskConfig(AppConfig):
|
||||
name = 'helpdesk'
|
||||
verbose_name = "Helpdesk"
|
||||
|
||||
|
@ -6,6 +6,8 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
forms.py - Definitions of newforms-based forms for creating and maintaining
|
||||
tickets.
|
||||
"""
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
try:
|
||||
from StringIO import StringIO
|
||||
except ImportError:
|
||||
@ -13,27 +15,27 @@ except ImportError:
|
||||
|
||||
from django import forms
|
||||
from django.forms import extras
|
||||
from django.core.files.storage import default_storage
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
try:
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
except ImportError:
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.auth import get_user_model
|
||||
try:
|
||||
from django.utils import timezone
|
||||
except ImportError:
|
||||
from datetime import datetime as timezone
|
||||
|
||||
from helpdesk.lib import send_templated_mail, safe_template_context
|
||||
from helpdesk.models import Ticket, Queue, FollowUp, Attachment, IgnoreEmail, TicketCC, CustomField, TicketCustomFieldValue, TicketDependency
|
||||
from helpdesk.models import (Ticket, Queue, FollowUp, Attachment, IgnoreEmail, TicketCC,
|
||||
CustomField, TicketCustomFieldValue, TicketDependency)
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class CustomFieldMixin(object):
|
||||
"""
|
||||
Mixin that provides a method to turn CustomFields into an actual field
|
||||
"""
|
||||
|
||||
def customfield_to_field(self, field, instanceargs):
|
||||
if field.data_type == 'varchar':
|
||||
fieldclass = forms.CharField
|
||||
@ -52,7 +54,7 @@ class CustomFieldMixin(object):
|
||||
fieldclass = forms.ChoiceField
|
||||
choices = field.choices_as_array
|
||||
if field.empty_selection_list:
|
||||
choices.insert(0, ('','---------' ) )
|
||||
choices.insert(0, ('', '---------'))
|
||||
instanceargs['choices'] = choices
|
||||
elif field.data_type == 'boolean':
|
||||
fieldclass = forms.BooleanField
|
||||
@ -73,11 +75,13 @@ class CustomFieldMixin(object):
|
||||
|
||||
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
|
||||
@ -91,103 +95,104 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
||||
except TicketCustomFieldValue.DoesNotExist:
|
||||
initial_value = None
|
||||
instanceargs = {
|
||||
'label': field.label,
|
||||
'help_text': field.help_text,
|
||||
'required': field.required,
|
||||
'initial': initial_value,
|
||||
}
|
||||
'label': field.label,
|
||||
'help_text': field.help_text,
|
||||
'required': field.required,
|
||||
'initial': initial_value,
|
||||
}
|
||||
|
||||
self.customfield_to_field(field, instanceargs)
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
|
||||
for field, value in self.cleaned_data.items():
|
||||
if field.startswith('custom_'):
|
||||
field_name = field.replace('custom_', '', 1)
|
||||
customfield = CustomField.objects.get(name=field_name)
|
||||
try:
|
||||
cfv = TicketCustomFieldValue.objects.get(ticket=self.instance, field=customfield)
|
||||
except:
|
||||
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):
|
||||
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 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 TicketForm(CustomFieldMixin, forms.Form):
|
||||
queue = forms.ChoiceField(
|
||||
label=_('Queue'),
|
||||
required=True,
|
||||
choices=()
|
||||
)
|
||||
)
|
||||
|
||||
title = forms.CharField(
|
||||
max_length=100,
|
||||
required=True,
|
||||
widget=forms.TextInput(attrs={'size':'60'}),
|
||||
widget=forms.TextInput(attrs={'size': '60'}),
|
||||
label=_('Summary of the problem'),
|
||||
)
|
||||
)
|
||||
|
||||
submitter_email = forms.EmailField(
|
||||
required=False,
|
||||
label=_('Submitter E-Mail Address'),
|
||||
widget=forms.TextInput(attrs={'size':'60'}),
|
||||
widget=forms.TextInput(attrs={'size': '60'}),
|
||||
help_text=_('This e-mail address will receive copies of all public '
|
||||
'updates to this ticket.'),
|
||||
)
|
||||
'updates to this ticket.'),
|
||||
)
|
||||
|
||||
body = forms.CharField(
|
||||
widget=forms.Textarea(attrs={'cols': 47, 'rows': 15}),
|
||||
label=_('Description of Issue'),
|
||||
required=True,
|
||||
)
|
||||
)
|
||||
|
||||
assigned_to = forms.ChoiceField(
|
||||
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.'),
|
||||
)
|
||||
'e-mailed details of this ticket immediately.'),
|
||||
)
|
||||
|
||||
priority = forms.ChoiceField(
|
||||
choices=Ticket.PRIORITY_CHOICES,
|
||||
required=False,
|
||||
initial='3',
|
||||
label=_('Priority'),
|
||||
help_text=_('Please select a priority carefully. If unsure, leave it '
|
||||
'as \'3\'.'),
|
||||
)
|
||||
help_text=_('Please select a priority carefully. If unsure, leave it as \'3\'.'),
|
||||
)
|
||||
|
||||
due_date = forms.DateTimeField(
|
||||
widget=extras.SelectDateWidget,
|
||||
required=False,
|
||||
label=_('Due on'),
|
||||
)
|
||||
)
|
||||
|
||||
def clean_due_date(self):
|
||||
data = self.cleaned_data['due_date']
|
||||
#TODO: add Google calendar update hook
|
||||
#if not hasattr(self, 'instance') or self.instance.due_date != new_data:
|
||||
# print "you changed!"
|
||||
# TODO: add Google calendar update hook
|
||||
# if not hasattr(self, 'instance') or self.instance.due_date != new_data:
|
||||
# print "you changed!"
|
||||
return data
|
||||
|
||||
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 __init__(self, *args, **kwargs):
|
||||
"""
|
||||
@ -196,14 +201,13 @@ class TicketForm(CustomFieldMixin, forms.Form):
|
||||
super(TicketForm, self).__init__(*args, **kwargs)
|
||||
for field in CustomField.objects.all():
|
||||
instanceargs = {
|
||||
'label': field.label,
|
||||
'help_text': field.help_text,
|
||||
'required': field.required,
|
||||
}
|
||||
'label': field.label,
|
||||
'help_text': field.help_text,
|
||||
'required': field.required,
|
||||
}
|
||||
|
||||
self.customfield_to_field(field, instanceargs)
|
||||
|
||||
|
||||
def save(self, user):
|
||||
"""
|
||||
Writes and returns a Ticket() object
|
||||
@ -211,15 +215,15 @@ class TicketForm(CustomFieldMixin, forms.Form):
|
||||
|
||||
q = Queue.objects.get(id=int(self.cleaned_data['queue']))
|
||||
|
||||
t = Ticket( title = self.cleaned_data['title'],
|
||||
submitter_email = self.cleaned_data['submitter_email'],
|
||||
created = timezone.now(),
|
||||
status = Ticket.OPEN_STATUS,
|
||||
queue = q,
|
||||
description = self.cleaned_data['body'],
|
||||
priority = self.cleaned_data['priority'],
|
||||
due_date = self.cleaned_data['due_date'],
|
||||
)
|
||||
t = Ticket(title=self.cleaned_data['title'],
|
||||
submitter_email=self.cleaned_data['submitter_email'],
|
||||
created=timezone.now(),
|
||||
status=Ticket.OPEN_STATUS,
|
||||
queue=q,
|
||||
description=self.cleaned_data['body'],
|
||||
priority=self.cleaned_data['priority'],
|
||||
due_date=self.cleaned_data['due_date'],
|
||||
)
|
||||
|
||||
if self.cleaned_data['assigned_to']:
|
||||
try:
|
||||
@ -228,22 +232,22 @@ class TicketForm(CustomFieldMixin, forms.Form):
|
||||
except User.DoesNotExist:
|
||||
t.assigned_to = None
|
||||
t.save()
|
||||
|
||||
|
||||
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)
|
||||
cfv = TicketCustomFieldValue(ticket=t,
|
||||
field=customfield,
|
||||
value=value)
|
||||
field=customfield,
|
||||
value=value)
|
||||
cfv.save()
|
||||
|
||||
f = FollowUp( ticket = t,
|
||||
title = _('Ticket Opened'),
|
||||
date = timezone.now(),
|
||||
public = True,
|
||||
comment = self.cleaned_data['body'],
|
||||
user = user,
|
||||
f = FollowUp(ticket=t,
|
||||
title=_('Ticket Opened'),
|
||||
date=timezone.now(),
|
||||
public=True,
|
||||
comment=self.cleaned_data['body'],
|
||||
user=user,
|
||||
)
|
||||
if self.cleaned_data['assigned_to']:
|
||||
f.title = _('Ticket Opened & Assigned to %(name)s') % {
|
||||
@ -251,7 +255,7 @@ class TicketForm(CustomFieldMixin, forms.Form):
|
||||
}
|
||||
|
||||
f.save()
|
||||
|
||||
|
||||
files = []
|
||||
if self.cleaned_data['attachment']:
|
||||
import mimetypes
|
||||
@ -262,12 +266,12 @@ class TicketForm(CustomFieldMixin, forms.Form):
|
||||
filename=filename,
|
||||
mime_type=mimetypes.guess_type(filename)[0] or 'application/octet-stream',
|
||||
size=file.size,
|
||||
)
|
||||
)
|
||||
a.file.save(file.name, file, save=False)
|
||||
a.save()
|
||||
|
||||
|
||||
if file.size < getattr(settings, 'MAX_EMAIL_ATTACHMENT_SIZE', 512000):
|
||||
# Only files smaller than 512kb (or as defined in
|
||||
# Only files smaller than 512kb (or as defined in
|
||||
# settings.MAX_EMAIL_ATTACHMENT_SIZE) are sent via email.
|
||||
try:
|
||||
files.append([a.filename, a.file])
|
||||
@ -276,7 +280,7 @@ class TicketForm(CustomFieldMixin, forms.Form):
|
||||
|
||||
context = safe_template_context(t)
|
||||
context['comment'] = f.comment
|
||||
|
||||
|
||||
messages_sent_to = []
|
||||
|
||||
if t.submitter_email:
|
||||
@ -287,10 +291,14 @@ class TicketForm(CustomFieldMixin, forms.Form):
|
||||
sender=q.from_address,
|
||||
fail_silently=True,
|
||||
files=files,
|
||||
)
|
||||
)
|
||||
messages_sent_to.append(t.submitter_email)
|
||||
|
||||
if t.assigned_to and t.assigned_to != user and t.assigned_to.usersettings.settings.get('email_on_ticket_assign', False) and t.assigned_to.email and t.assigned_to.email not in messages_sent_to:
|
||||
if t.assigned_to and \
|
||||
t.assigned_to != user and \
|
||||
t.assigned_to.usersettings.settings.get('email_on_ticket_assign', False) and \
|
||||
t.assigned_to.email and \
|
||||
t.assigned_to.email not in messages_sent_to:
|
||||
send_templated_mail(
|
||||
'assigned_owner',
|
||||
context,
|
||||
@ -298,7 +306,7 @@ class TicketForm(CustomFieldMixin, forms.Form):
|
||||
sender=q.from_address,
|
||||
fail_silently=True,
|
||||
files=files,
|
||||
)
|
||||
)
|
||||
messages_sent_to.append(t.assigned_to.email)
|
||||
|
||||
if q.new_ticket_cc and q.new_ticket_cc not in messages_sent_to:
|
||||
@ -309,10 +317,12 @@ class TicketForm(CustomFieldMixin, forms.Form):
|
||||
sender=q.from_address,
|
||||
fail_silently=True,
|
||||
files=files,
|
||||
)
|
||||
)
|
||||
messages_sent_to.append(q.new_ticket_cc)
|
||||
|
||||
if q.updated_ticket_cc and q.updated_ticket_cc != q.new_ticket_cc and q.updated_ticket_cc not in messages_sent_to:
|
||||
if q.updated_ticket_cc and \
|
||||
q.updated_ticket_cc != q.new_ticket_cc and \
|
||||
q.updated_ticket_cc not in messages_sent_to:
|
||||
send_templated_mail(
|
||||
'newticket_cc',
|
||||
context,
|
||||
@ -320,7 +330,7 @@ class TicketForm(CustomFieldMixin, forms.Form):
|
||||
sender=q.from_address,
|
||||
fail_silently=True,
|
||||
files=files,
|
||||
)
|
||||
)
|
||||
|
||||
return t
|
||||
|
||||
@ -330,28 +340,28 @@ class PublicTicketForm(CustomFieldMixin, forms.Form):
|
||||
label=_('Queue'),
|
||||
required=True,
|
||||
choices=()
|
||||
)
|
||||
)
|
||||
|
||||
title = forms.CharField(
|
||||
max_length=100,
|
||||
required=True,
|
||||
widget=forms.TextInput(),
|
||||
label=_('Summary of your query'),
|
||||
)
|
||||
)
|
||||
|
||||
submitter_email = forms.EmailField(
|
||||
required=True,
|
||||
label=_('Your E-Mail Address'),
|
||||
help_text=_('We will e-mail you when your ticket is updated.'),
|
||||
)
|
||||
)
|
||||
|
||||
body = forms.CharField(
|
||||
widget=forms.Textarea(),
|
||||
label=_('Description of your issue'),
|
||||
required=True,
|
||||
help_text=_('Please be as descriptive as possible, including any '
|
||||
'details we may need to address your query.'),
|
||||
)
|
||||
'details we may need to address your query.'),
|
||||
)
|
||||
|
||||
priority = forms.ChoiceField(
|
||||
choices=Ticket.PRIORITY_CHOICES,
|
||||
@ -359,20 +369,20 @@ class PublicTicketForm(CustomFieldMixin, forms.Form):
|
||||
initial='3',
|
||||
label=_('Urgency'),
|
||||
help_text=_('Please select a priority carefully.'),
|
||||
)
|
||||
)
|
||||
|
||||
due_date = forms.DateTimeField(
|
||||
widget=extras.SelectDateWidget,
|
||||
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.'),
|
||||
max_length=1000,
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
@ -381,10 +391,10 @@ class PublicTicketForm(CustomFieldMixin, forms.Form):
|
||||
super(PublicTicketForm, self).__init__(*args, **kwargs)
|
||||
for field in CustomField.objects.filter(staff_only=False):
|
||||
instanceargs = {
|
||||
'label': field.label,
|
||||
'help_text': field.help_text,
|
||||
'required': field.required,
|
||||
}
|
||||
'label': field.label,
|
||||
'help_text': field.help_text,
|
||||
'required': field.required,
|
||||
}
|
||||
|
||||
self.customfield_to_field(field, instanceargs)
|
||||
|
||||
@ -396,15 +406,15 @@ class PublicTicketForm(CustomFieldMixin, forms.Form):
|
||||
q = Queue.objects.get(id=int(self.cleaned_data['queue']))
|
||||
|
||||
t = Ticket(
|
||||
title = self.cleaned_data['title'],
|
||||
submitter_email = self.cleaned_data['submitter_email'],
|
||||
created = timezone.now(),
|
||||
status = Ticket.OPEN_STATUS,
|
||||
queue = q,
|
||||
description = self.cleaned_data['body'],
|
||||
priority = self.cleaned_data['priority'],
|
||||
due_date = self.cleaned_data['due_date'],
|
||||
)
|
||||
title=self.cleaned_data['title'],
|
||||
submitter_email=self.cleaned_data['submitter_email'],
|
||||
created=timezone.now(),
|
||||
status=Ticket.OPEN_STATUS,
|
||||
queue=q,
|
||||
description=self.cleaned_data['body'],
|
||||
priority=self.cleaned_data['priority'],
|
||||
due_date=self.cleaned_data['due_date'],
|
||||
)
|
||||
|
||||
if q.default_owner and not t.assigned_to:
|
||||
t.assigned_to = q.default_owner
|
||||
@ -416,17 +426,17 @@ class PublicTicketForm(CustomFieldMixin, forms.Form):
|
||||
field_name = field.replace('custom_', '', 1)
|
||||
customfield = CustomField.objects.get(name=field_name)
|
||||
cfv = TicketCustomFieldValue(ticket=t,
|
||||
field=customfield,
|
||||
value=value)
|
||||
field=customfield,
|
||||
value=value)
|
||||
cfv.save()
|
||||
|
||||
f = FollowUp(
|
||||
ticket = t,
|
||||
title = _('Ticket Opened Via Web'),
|
||||
date = timezone.now(),
|
||||
public = True,
|
||||
comment = self.cleaned_data['body'],
|
||||
)
|
||||
ticket=t,
|
||||
title=_('Ticket Opened Via Web'),
|
||||
date=timezone.now(),
|
||||
public=True,
|
||||
comment=self.cleaned_data['body'],
|
||||
)
|
||||
|
||||
f.save()
|
||||
|
||||
@ -440,12 +450,12 @@ class PublicTicketForm(CustomFieldMixin, forms.Form):
|
||||
filename=filename,
|
||||
mime_type=mimetypes.guess_type(filename)[0] or 'application/octet-stream',
|
||||
size=file.size,
|
||||
)
|
||||
)
|
||||
a.file.save(file.name, file, save=False)
|
||||
a.save()
|
||||
|
||||
|
||||
if file.size < getattr(settings, 'MAX_EMAIL_ATTACHMENT_SIZE', 512000):
|
||||
# Only files smaller than 512kb (or as defined in
|
||||
# Only files smaller than 512kb (or as defined in
|
||||
# settings.MAX_EMAIL_ATTACHMENT_SIZE) are sent via email.
|
||||
files.append([a.filename, a.file])
|
||||
|
||||
@ -460,10 +470,13 @@ class PublicTicketForm(CustomFieldMixin, forms.Form):
|
||||
sender=q.from_address,
|
||||
fail_silently=True,
|
||||
files=files,
|
||||
)
|
||||
)
|
||||
messages_sent_to.append(t.submitter_email)
|
||||
|
||||
if t.assigned_to and t.assigned_to.usersettings.settings.get('email_on_ticket_assign', False) and t.assigned_to.email and t.assigned_to.email not in messages_sent_to:
|
||||
if t.assigned_to and \
|
||||
t.assigned_to.usersettings.settings.get('email_on_ticket_assign', False) and \
|
||||
t.assigned_to.email and \
|
||||
t.assigned_to.email not in messages_sent_to:
|
||||
send_templated_mail(
|
||||
'assigned_owner',
|
||||
context,
|
||||
@ -471,7 +484,7 @@ class PublicTicketForm(CustomFieldMixin, forms.Form):
|
||||
sender=q.from_address,
|
||||
fail_silently=True,
|
||||
files=files,
|
||||
)
|
||||
)
|
||||
messages_sent_to.append(t.assigned_to.email)
|
||||
|
||||
if q.new_ticket_cc and q.new_ticket_cc not in messages_sent_to:
|
||||
@ -482,10 +495,12 @@ class PublicTicketForm(CustomFieldMixin, forms.Form):
|
||||
sender=q.from_address,
|
||||
fail_silently=True,
|
||||
files=files,
|
||||
)
|
||||
)
|
||||
messages_sent_to.append(q.new_ticket_cc)
|
||||
|
||||
if q.updated_ticket_cc and q.updated_ticket_cc != q.new_ticket_cc and q.updated_ticket_cc not in messages_sent_to:
|
||||
if q.updated_ticket_cc and \
|
||||
q.updated_ticket_cc != q.new_ticket_cc and \
|
||||
q.updated_ticket_cc not in messages_sent_to:
|
||||
send_templated_mail(
|
||||
'newticket_cc',
|
||||
context,
|
||||
@ -493,7 +508,7 @@ class PublicTicketForm(CustomFieldMixin, forms.Form):
|
||||
sender=q.from_address,
|
||||
fail_silently=True,
|
||||
files=files,
|
||||
)
|
||||
)
|
||||
|
||||
return t
|
||||
|
||||
@ -503,25 +518,25 @@ class UserSettingsForm(forms.Form):
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
email_on_ticket_apichange = forms.BooleanField(
|
||||
label=_('E-mail me when a ticket is changed via the API?'),
|
||||
help_text=_('If a ticket is altered by the API, do you want to receive an e-mail?'),
|
||||
required=False,
|
||||
)
|
||||
)
|
||||
|
||||
tickets_per_page = forms.IntegerField(
|
||||
label=_('Number of tickets to show per page'),
|
||||
@ -529,32 +544,39 @@ class UserSettingsForm(forms.Form):
|
||||
required=False,
|
||||
min_value=1,
|
||||
max_value=1000,
|
||||
)
|
||||
)
|
||||
|
||||
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):
|
||||
|
||||
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 Meta:
|
||||
model = TicketCC
|
||||
exclude = ('ticket',)
|
||||
self.fields['user'].queryset = users
|
||||
|
||||
|
||||
class TicketDependencyForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = TicketDependency
|
||||
exclude = ('ticket',)
|
||||
|
@ -6,7 +6,7 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
lib.py - Common functions (eg multipart e-mail)
|
||||
"""
|
||||
|
||||
chart_colours = ('80C65A', '990066', 'FF9900', '3399CC', 'BBCCED', '3399CC', 'FFCC33')
|
||||
import logging
|
||||
|
||||
try:
|
||||
from base64 import urlsafe_b64encode as b64encode
|
||||
@ -17,13 +17,20 @@ try:
|
||||
except ImportError:
|
||||
from base64 import decodestring as b64decode
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger('helpdesk')
|
||||
|
||||
from django.utils.encoding import smart_str
|
||||
from django.db.models import Q
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
def send_templated_mail(template_name, email_context, recipients, sender=None, bcc=None, fail_silently=False, files=None):
|
||||
logger = logging.getLogger('helpdesk')
|
||||
|
||||
|
||||
def send_templated_mail(template_name,
|
||||
email_context,
|
||||
recipients,
|
||||
sender=None,
|
||||
bcc=None,
|
||||
fail_silently=False,
|
||||
files=None):
|
||||
"""
|
||||
send_templated_mail() is a warpper around Django's e-mail routines that
|
||||
allows us to easily send multipart (text/plain & text/html) e-mails using
|
||||
@ -83,16 +90,17 @@ def send_templated_mail(template_name, email_context, recipients, sender=None, b
|
||||
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
|
||||
logger.warning('template "%s" does not exist, no mail sent',
|
||||
template_name)
|
||||
return # just ignore if template doesn't exist
|
||||
|
||||
if not sender:
|
||||
sender = settings.DEFAULT_FROM_EMAIL
|
||||
|
||||
footer_file = os.path.join('helpdesk', locale, 'email_text_footer.txt')
|
||||
|
||||
# get_template_from_string was removed in Django 1.8 http://django.readthedocs.org/en/1.8.x/ref/templates/upgrading.html
|
||||
# get_template_from_string was removed in Django 1.8
|
||||
# http://django.readthedocs.org/en/1.8.x/ref/templates/upgrading.html
|
||||
try:
|
||||
from django.template import engines
|
||||
template_func = engines['django'].from_string
|
||||
@ -101,25 +109,26 @@ def send_templated_mail(template_name, email_context, recipients, sender=None, b
|
||||
|
||||
text_part = template_func(
|
||||
"%s{%% include '%s' %%}" % (t.plain_text, footer_file)
|
||||
).render(context)
|
||||
).render(context)
|
||||
|
||||
email_html_base_file = os.path.join('helpdesk', locale, 'email_html_base.html')
|
||||
|
||||
|
||||
''' keep new lines in html emails '''
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
# keep new lines in html emails
|
||||
if 'comment' in context:
|
||||
html_txt = context['comment']
|
||||
html_txt = html_txt.replace('\r\n', '<br>')
|
||||
context['comment'] = mark_safe(html_txt)
|
||||
|
||||
# get_template_from_string was removed in Django 1.8 http://django.readthedocs.org/en/1.8.x/ref/templates/upgrading.html
|
||||
# get_template_from_string was removed in Django 1.8
|
||||
# http://django.readthedocs.org/en/1.8.x/ref/templates/upgrading.html
|
||||
html_part = template_func(
|
||||
"{%% extends '%s' %%}{%% block title %%}%s{%% endblock %%}{%% block content %%}%s{%% endblock %%}" % (email_html_base_file, t.heading, t.html)
|
||||
).render(context)
|
||||
"{%% extends '%s' %%}{%% block title %%}"
|
||||
"%s"
|
||||
"{%% endblock %%}{%% block content %%}%s{%% endblock %%}" %
|
||||
(email_html_base_file, t.heading, t.html)).render(context)
|
||||
|
||||
# get_template_from_string was removed in Django 1.8 http://django.readthedocs.org/en/1.8.x/ref/templates/upgrading.html
|
||||
# get_template_from_string was removed in Django 1.8
|
||||
# http://django.readthedocs.org/en/1.8.x/ref/templates/upgrading.html
|
||||
subject_part = template_func(
|
||||
HELPDESK_EMAIL_SUBJECT_TEMPLATE % {
|
||||
"subject": t.subject,
|
||||
@ -129,13 +138,11 @@ def send_templated_mail(template_name, email_context, recipients, sender=None, b
|
||||
if recipients.find(','):
|
||||
recipients = recipients.split(',')
|
||||
elif type(recipients) != list:
|
||||
recipients = [recipients,]
|
||||
recipients = [recipients, ]
|
||||
|
||||
msg = EmailMultiAlternatives( subject_part.replace('\n', '').replace('\r', ''),
|
||||
text_part,
|
||||
sender,
|
||||
recipients,
|
||||
bcc=bcc)
|
||||
msg = EmailMultiAlternatives(
|
||||
subject_part.replace('\n', '').replace('\r', ''),
|
||||
text_part, sender, recipients, bcc=bcc)
|
||||
msg.attach_alternative(html_part, "text/html")
|
||||
|
||||
if files:
|
||||
@ -226,23 +233,23 @@ def safe_template_context(ticket):
|
||||
|
||||
context = {
|
||||
'queue': {},
|
||||
'ticket': {},
|
||||
}
|
||||
'ticket': {}
|
||||
}
|
||||
queue = ticket.queue
|
||||
|
||||
for field in ( 'title', 'slug', 'email_address', 'from_address', 'locale'):
|
||||
for field in ('title', 'slug', 'email_address', 'from_address', 'locale'):
|
||||
attr = getattr(queue, field, None)
|
||||
if callable(attr):
|
||||
context['queue'][field] = attr()
|
||||
else:
|
||||
context['queue'][field] = attr
|
||||
|
||||
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'
|
||||
):
|
||||
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['ticket'][field] = '%s' % attr()
|
||||
@ -278,10 +285,10 @@ def text_is_spam(text, request):
|
||||
)
|
||||
|
||||
if hasattr(settings, 'TYPEPAD_ANTISPAM_API_KEY'):
|
||||
ak.setAPIKey(key = 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)
|
||||
ak.setAPIKey(key=settings.AKISMET_API_KEY)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
@ -16,12 +16,12 @@ from optparse import make_option
|
||||
import sys
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db.models import Q
|
||||
|
||||
from helpdesk.models import EscalationExclusion, Queue
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
def __init__(self):
|
||||
BaseCommand.__init__(self)
|
||||
|
||||
@ -43,11 +43,12 @@ class Command(BaseCommand):
|
||||
default=False,
|
||||
dest='escalate-verbosely',
|
||||
help='Display a list of dates excluded'),
|
||||
)
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
days = options['days']
|
||||
occurrences = options['occurrences']
|
||||
# optparse should already handle the `or 1`
|
||||
occurrences = options['occurrences'] or 1
|
||||
verbose = False
|
||||
queue_slugs = options['queues']
|
||||
queues = []
|
||||
@ -55,8 +56,6 @@ class Command(BaseCommand):
|
||||
if options['escalate-verbosely']:
|
||||
verbose = True
|
||||
|
||||
# this should already be handled by optparse
|
||||
if not occurrences: occurrences = 1
|
||||
if not (days and occurrences):
|
||||
raise CommandError('One or more occurrences must be specified.')
|
||||
|
||||
@ -116,7 +115,6 @@ def usage():
|
||||
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:
|
||||
@ -126,7 +124,7 @@ if __name__ == '__main__':
|
||||
sys.exit(2)
|
||||
|
||||
days = None
|
||||
occurrences = None
|
||||
occurrences = 1
|
||||
verbose = False
|
||||
queue_slugs = None
|
||||
queues = []
|
||||
@ -139,9 +137,8 @@ if __name__ == '__main__':
|
||||
if o in ('-q', '--queues'):
|
||||
queue_slugs = a
|
||||
if o in ('-o', '--occurrences'):
|
||||
occurrences = int(a)
|
||||
occurrences = int(a) or 1
|
||||
|
||||
if not occurrences: occurrences = 1
|
||||
if not (days and occurrences):
|
||||
usage()
|
||||
sys.exit(2)
|
||||
|
@ -25,6 +25,7 @@ from helpdesk.models import Queue
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
def __init__(self):
|
||||
BaseCommand.__init__(self)
|
||||
|
||||
@ -32,7 +33,7 @@ class Command(BaseCommand):
|
||||
make_option(
|
||||
'--queues', '-q',
|
||||
help='Queues to include (default: all). Use queue slugs'),
|
||||
)
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
queue_slugs = options['queues']
|
||||
@ -71,4 +72,3 @@ class Command(BaseCommand):
|
||||
)
|
||||
except IntegrityError:
|
||||
self.stdout.write(" .. permission already existed, skipping")
|
||||
|
||||
|
@ -4,23 +4,22 @@ 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
|
||||
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
|
||||
try:
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
except ImportError:
|
||||
from django.contrib.auth.models import User
|
||||
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"
|
||||
"""create_usersettings command"""
|
||||
|
||||
help = _('Check for user without django-helpdesk UserSettings '
|
||||
'and create settings if required. Uses '
|
||||
@ -28,10 +27,7 @@ class Command(BaseCommand):
|
||||
'suit your situation.')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"handle command line"
|
||||
"""handle command line"""
|
||||
for u in User.objects.all():
|
||||
try:
|
||||
s = UserSettings.objects.get(user=u)
|
||||
except UserSettings.DoesNotExist:
|
||||
s = UserSettings(user=u, settings=DEFAULT_USER_SETTINGS)
|
||||
s.save()
|
||||
UserSettings.objects.get_or_create(user=u,
|
||||
defaults={'settings': DEFAULT_USER_SETTINGS})
|
||||
|
@ -28,6 +28,7 @@ from helpdesk.lib import send_templated_mail, safe_template_context
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
def __init__(self):
|
||||
BaseCommand.__init__(self)
|
||||
|
||||
@ -40,7 +41,7 @@ class Command(BaseCommand):
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Display a list of dates excluded'),
|
||||
)
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
verbose = False
|
||||
@ -56,7 +57,7 @@ class Command(BaseCommand):
|
||||
queue_set = queue_slugs.split(',')
|
||||
for queue in queue_set:
|
||||
try:
|
||||
q = Queue.objects.get(slug__exact=queue)
|
||||
Queue.objects.get(slug__exact=queue)
|
||||
except Queue.DoesNotExist:
|
||||
raise CommandError("Queue %s does not exist." % queue)
|
||||
queues.append(queue)
|
||||
@ -82,24 +83,23 @@ def escalate_tickets(queues, verbose):
|
||||
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)
|
||||
):
|
||||
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
|
||||
@ -114,7 +114,7 @@ def escalate_tickets(queues, verbose):
|
||||
recipients=t.submitter_email,
|
||||
sender=t.queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
)
|
||||
|
||||
if t.queue.updated_ticket_cc:
|
||||
send_templated_mail(
|
||||
@ -123,7 +123,7 @@ def escalate_tickets(queues, verbose):
|
||||
recipients=t.queue.updated_ticket_cc,
|
||||
sender=t.queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
)
|
||||
|
||||
if t.assigned_to:
|
||||
send_templated_mail(
|
||||
@ -132,19 +132,19 @@ def escalate_tickets(queues, verbose):
|
||||
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 + 1,
|
||||
t.priority
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
f = FollowUp(
|
||||
ticket = t,
|
||||
title = 'Ticket Escalated',
|
||||
ticket=t,
|
||||
title='Ticket Escalated',
|
||||
date=timezone.now(),
|
||||
public=True,
|
||||
comment=_('Ticket escalated after %s days' % q.escalate_days),
|
||||
@ -152,10 +152,10 @@ def escalate_tickets(queues, verbose):
|
||||
f.save()
|
||||
|
||||
tc = TicketChange(
|
||||
followup = f,
|
||||
field = _('Priority'),
|
||||
old_value = t.priority + 1,
|
||||
new_value = t.priority,
|
||||
followup=f,
|
||||
field=_('Priority'),
|
||||
old_value=t.priority + 1,
|
||||
new_value=t.priority,
|
||||
)
|
||||
tc.save()
|
||||
|
||||
|
@ -40,7 +40,17 @@ from helpdesk.lib import send_templated_mail, safe_template_context
|
||||
from helpdesk.models import Queue, Ticket, FollowUp, Attachment, IgnoreEmail
|
||||
|
||||
|
||||
STRIPPED_SUBJECT_STRINGS = [
|
||||
"Re: ",
|
||||
"Fw: ",
|
||||
"RE: ",
|
||||
"FW: ",
|
||||
"Automatic reply: ",
|
||||
]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
def __init__(self):
|
||||
BaseCommand.__init__(self)
|
||||
|
||||
@ -50,9 +60,10 @@ class Command(BaseCommand):
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Hide details about each queue/message as they are processed'),
|
||||
)
|
||||
)
|
||||
|
||||
help = 'Process Jutda Helpdesk queues and process e-mails via POP3/IMAP as required, feeding them into the helpdesk.'
|
||||
help = 'Process Jutda Helpdesk queues and process e-mails via ' \
|
||||
'POP3/IMAP as required, feeding them into the helpdesk.'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
quiet = options.get('quiet', False)
|
||||
@ -65,12 +76,11 @@ def process_email(quiet=False):
|
||||
allow_email_submission=True):
|
||||
|
||||
if not q.email_box_last_check:
|
||||
q.email_box_last_check = timezone.now()-timedelta(minutes=30)
|
||||
q.email_box_last_check = timezone.now() - timedelta(minutes=30)
|
||||
|
||||
if not q.email_box_interval:
|
||||
q.email_box_interval = 0
|
||||
|
||||
|
||||
queue_time_delta = timedelta(minutes=q.email_box_interval)
|
||||
|
||||
if (q.email_box_last_check + queue_time_delta) > timezone.now():
|
||||
@ -90,39 +100,47 @@ def process_queue(q, quiet=False):
|
||||
try:
|
||||
import socks
|
||||
except ImportError:
|
||||
raise ImportError("Queue has been configured with proxy settings, but no socks library was installed. Try to install PySocks via pypi.")
|
||||
raise ImportError("Queue has been configured with proxy settings, "
|
||||
"but no socks library was installed. "
|
||||
"Try to install PySocks via pypi.")
|
||||
|
||||
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)
|
||||
socks.set_default_proxy(proxy_type=proxy_type,
|
||||
addr=q.socks_proxy_host,
|
||||
port=q.socks_proxy_port)
|
||||
socket.socket = socks.socksocket
|
||||
else:
|
||||
socket.socket = socket._socketobject
|
||||
|
||||
email_box_type = settings.QUEUE_EMAIL_BOX_TYPE if settings.QUEUE_EMAIL_BOX_TYPE else q.email_box_type
|
||||
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))
|
||||
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))
|
||||
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))
|
||||
|
||||
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]
|
||||
|
||||
for msg in messagesInfo:
|
||||
msgNum = msg.split(" ")[0]
|
||||
msgSize = msg.split(" ")[1]
|
||||
# msgSize = msg.split(" ")[1]
|
||||
|
||||
full_message = "\n".join(server.retr(msgNum)[1])
|
||||
ticket = ticket_from_message(message=full_message, queue=q, quiet=quiet)
|
||||
@ -134,13 +152,22 @@ def process_queue(q, quiet=False):
|
||||
|
||||
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))
|
||||
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))
|
||||
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))
|
||||
|
||||
server.login(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER, q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD)
|
||||
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)
|
||||
|
||||
status, data = server.search(None, 'NOT', 'DELETED')
|
||||
@ -151,7 +178,7 @@ def process_queue(q, quiet=False):
|
||||
ticket = ticket_from_message(message=data[0][1], queue=q, quiet=quiet)
|
||||
if ticket:
|
||||
server.store(num, '+FLAGS', '\\Deleted')
|
||||
|
||||
|
||||
server.expunge()
|
||||
server.close()
|
||||
server.logout()
|
||||
@ -160,22 +187,26 @@ def process_queue(q, quiet=False):
|
||||
def decodeUnknown(charset, string):
|
||||
if not charset:
|
||||
try:
|
||||
return string.decode('utf-8','ignore')
|
||||
return string.decode('utf-8', 'ignore')
|
||||
except:
|
||||
return string.decode('iso8859-1','ignore')
|
||||
return string.decode('iso8859-1', 'ignore')
|
||||
return unicode(string, charset)
|
||||
|
||||
|
||||
def decode_mail_headers(string):
|
||||
decoded = decode_header(string)
|
||||
return u' '.join([unicode(msg, charset or 'utf-8') for msg, charset in decoded])
|
||||
|
||||
|
||||
def ticket_from_message(message, queue, quiet):
|
||||
# 'message' must be an RFC822 formatted message.
|
||||
msg = message
|
||||
message = email.message_from_string(msg)
|
||||
subject = message.get('subject', _('Created from e-mail'))
|
||||
subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject))
|
||||
subject = subject.replace("Re: ", "").replace("Fw: ", "").replace("RE: ", "").replace("FW: ", "").replace("Automatic reply: ", "").strip()
|
||||
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))
|
||||
@ -192,7 +223,7 @@ def ticket_from_message(message, queue, quiet):
|
||||
return False
|
||||
return True
|
||||
|
||||
matchobj = re.match(r".*\["+queue.slug+"-(?P<id>\d+)\]", subject)
|
||||
matchobj = re.match(r".*\[" + queue.slug + "-(?P<id>\d+)\]", subject)
|
||||
if matchobj:
|
||||
# This is a reply or forward.
|
||||
ticket = matchobj.group('id')
|
||||
@ -210,9 +241,10 @@ def ticket_from_message(message, queue, quiet):
|
||||
if name:
|
||||
name = collapse_rfc2231_value(name)
|
||||
|
||||
if part.get_content_maintype() == 'text' and name == None:
|
||||
if part.get_content_maintype() == 'text' and name is None:
|
||||
if part.get_content_subtype() == 'plain':
|
||||
body_plain = EmailReplyParser.parse_reply(decodeUnknown(part.get_content_charset(), part.get_payload(decode=True)))
|
||||
body_plain = EmailReplyParser.parse_reply(
|
||||
decodeUnknown(part.get_content_charset(), part.get_payload(decode=True)))
|
||||
else:
|
||||
body_html = part.get_payload(decode=True)
|
||||
else:
|
||||
@ -224,7 +256,7 @@ def ticket_from_message(message, queue, quiet):
|
||||
'filename': name,
|
||||
'content': part.get_payload(decode=True),
|
||||
'type': part.get_content_type()},
|
||||
)
|
||||
)
|
||||
|
||||
counter += 1
|
||||
|
||||
@ -259,7 +291,7 @@ def ticket_from_message(message, queue, quiet):
|
||||
if smtp_priority in high_priority_types or smtp_importance in high_priority_types:
|
||||
priority = 2
|
||||
|
||||
if ticket == None:
|
||||
if ticket is None:
|
||||
t = Ticket(
|
||||
title=subject,
|
||||
queue=queue,
|
||||
@ -270,24 +302,24 @@ def ticket_from_message(message, queue, quiet):
|
||||
)
|
||||
t.save()
|
||||
new = True
|
||||
update = ''
|
||||
# update = ''
|
||||
|
||||
elif t.status == Ticket.CLOSED_STATUS:
|
||||
t.status = Ticket.REOPENED_STATUS
|
||||
t.save()
|
||||
|
||||
f = FollowUp(
|
||||
ticket = t,
|
||||
title = _('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}),
|
||||
date = timezone.now(),
|
||||
public = True,
|
||||
comment = body,
|
||||
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()
|
||||
|
||||
if not quiet:
|
||||
@ -302,13 +334,12 @@ def ticket_from_message(message, queue, quiet):
|
||||
filename=filename,
|
||||
mime_type=file['type'],
|
||||
size=len(file['content']),
|
||||
)
|
||||
)
|
||||
a.file.save(filename, ContentFile(file['content']), save=False)
|
||||
a.save()
|
||||
if not quiet:
|
||||
print(" - %s" % filename)
|
||||
|
||||
|
||||
context = safe_template_context(t)
|
||||
|
||||
if new:
|
||||
@ -320,7 +351,7 @@ def ticket_from_message(message, queue, quiet):
|
||||
recipients=sender_email,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
)
|
||||
|
||||
if queue.new_ticket_cc:
|
||||
send_templated_mail(
|
||||
@ -329,7 +360,7 @@ def ticket_from_message(message, queue, quiet):
|
||||
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(
|
||||
@ -338,15 +369,15 @@ def ticket_from_message(message, queue, quiet):
|
||||
recipients=queue.updated_ticket_cc,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
context.update(comment=f.comment)
|
||||
|
||||
if t.status == Ticket.REOPENED_STATUS:
|
||||
update = _(' (Reopened)')
|
||||
else:
|
||||
update = _(' (Updated)')
|
||||
# if t.status == Ticket.REOPENED_STATUS:
|
||||
# update = _(' (Reopened)')
|
||||
# else:
|
||||
# update = _(' (Updated)')
|
||||
|
||||
if t.assigned_to:
|
||||
send_templated_mail(
|
||||
@ -355,7 +386,7 @@ def ticket_from_message(message, queue, quiet):
|
||||
recipients=t.assigned_to.email,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
)
|
||||
|
||||
if queue.updated_ticket_cc:
|
||||
send_templated_mail(
|
||||
@ -364,11 +395,10 @@ def ticket_from_message(message, queue, quiet):
|
||||
recipients=queue.updated_ticket_cc,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
)
|
||||
|
||||
return t
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
process_email()
|
||||
|
||||
|
@ -25,7 +25,7 @@ def load_fixture(apps, schema_editor):
|
||||
|
||||
|
||||
def unload_fixture(apps, schema_editor):
|
||||
"Delete all EmailTemplate objects"
|
||||
"""Delete all EmailTemplate objects"""
|
||||
|
||||
objects = deserialize_fixture()
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -11,22 +11,27 @@ try:
|
||||
except:
|
||||
DEFAULT_USER_SETTINGS = None
|
||||
|
||||
if type(DEFAULT_USER_SETTINGS) != type(dict()):
|
||||
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,
|
||||
'email_on_ticket_apichange': True,
|
||||
'tickets_per_page': 25
|
||||
}
|
||||
'use_email_as_submitter': True,
|
||||
'email_on_ticket_assign': True,
|
||||
'email_on_ticket_change': True,
|
||||
'login_view_ticketlist': True,
|
||||
'email_on_ticket_apichange': True,
|
||||
'tickets_per_page': 25
|
||||
}
|
||||
|
||||
|
||||
HAS_TAG_SUPPORT = False
|
||||
|
||||
''' generic options - visible on all pages '''
|
||||
##########################################
|
||||
# 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)
|
||||
HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(settings,
|
||||
'HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT',
|
||||
False)
|
||||
|
||||
# show knowledgebase links?
|
||||
HELPDESK_KB_ENABLED = getattr(settings, 'HELPDESK_KB_ENABLED', True)
|
||||
@ -34,14 +39,20 @@ 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
|
||||
# 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)
|
||||
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", "fr", "it", "ru"])
|
||||
# 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", "fr", "it", "ru"])
|
||||
|
||||
# show link to 'change password' on 'User Settings' page?
|
||||
HELPDESK_SHOW_CHANGE_PASSWORD = getattr(settings, 'HELPDESK_SHOW_CHANGE_PASSWORD', False)
|
||||
@ -50,10 +61,15 @@ HELPDESK_SHOW_CHANGE_PASSWORD = getattr(settings, 'HELPDESK_SHOW_CHANGE_PASSWORD
|
||||
HELPDESK_FOLLOWUP_MOD = getattr(settings, 'HELPDESK_FOLLOWUP_MOD', False)
|
||||
|
||||
# auto-subscribe user to ticket if (s)he responds to a ticket?
|
||||
HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE = getattr(settings, 'HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE', False)
|
||||
HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE = getattr(settings,
|
||||
'HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE',
|
||||
False)
|
||||
|
||||
|
||||
''' options for public pages '''
|
||||
############################
|
||||
# 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)
|
||||
|
||||
@ -61,17 +77,25 @@ HELPDESK_VIEW_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC
|
||||
HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_SUBMIT_A_TICKET_PUBLIC', True)
|
||||
|
||||
|
||||
###################################
|
||||
# options for update_ticket views #
|
||||
###################################
|
||||
|
||||
''' options for update_ticket views '''
|
||||
# allow non-staff users to interact with tickets? this will also change how 'staff_member_required'
|
||||
# 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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
@ -82,21 +106,28 @@ HELPDESK_STAFF_ONLY_TICKET_OWNERS = getattr(settings, 'HELPDESK_STAFF_ONLY_TICKE
|
||||
# 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")
|
||||
HELPDESK_EMAIL_SUBJECT_TEMPLATE = getattr(
|
||||
settings, 'HELPDESK_EMAIL_SUBJECT_TEMPLATE',
|
||||
"{{ ticket.ticket }} {{ ticket.title|safe }} %(subject)s")
|
||||
|
||||
# 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 '''
|
||||
########################################
|
||||
# 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)
|
||||
HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr(
|
||||
settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False)
|
||||
|
||||
|
||||
#################
|
||||
# email options #
|
||||
#################
|
||||
|
||||
''' 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)
|
||||
@ -104,6 +135,6 @@ 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 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)
|
||||
HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = getattr(
|
||||
settings, 'HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION', False)
|
||||
|
@ -17,8 +17,9 @@ Assuming 'food' = 'pizza' and 'best_foods' = ['pizza', 'pie', 'cake]:
|
||||
|
||||
from django import template
|
||||
|
||||
|
||||
def in_list(value, arg):
|
||||
return value in ( arg or [] )
|
||||
return value in (arg or [])
|
||||
|
||||
register = template.Library()
|
||||
register.filter(in_list)
|
||||
|
@ -1,20 +1,22 @@
|
||||
"""
|
||||
django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
templatetags/load_helpdesk_settings.py - returns the settings as defined in
|
||||
templatetags/load_helpdesk_settings.py - returns the settings as defined in
|
||||
django-helpdesk/helpdesk/settings.py
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
from django.template import Library
|
||||
from helpdesk import settings as helpdesk_settings_config
|
||||
|
||||
|
||||
def load_helpdesk_settings(request):
|
||||
try:
|
||||
return helpdesk_settings_config
|
||||
except Exception as e:
|
||||
import sys
|
||||
print >> sys.stderr, "'load_helpdesk_settings' template tag (django-helpdesk) crashed with following error:"
|
||||
print >> sys.stderr, e
|
||||
print("'load_helpdesk_settings' template tag (django-helpdesk) crashed with following error:",
|
||||
file=sys.stderr)
|
||||
print(e, file=sys.stderr)
|
||||
return ''
|
||||
|
||||
register = Library()
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
templatetags/saved_queries.py - This template tag returns previously saved
|
||||
templatetags/saved_queries.py - This template tag returns previously saved
|
||||
queries. Therefore you don't need to modify
|
||||
any views.
|
||||
"""
|
||||
@ -17,8 +17,8 @@ def saved_queries(user):
|
||||
return user_saved_queries
|
||||
except Exception as e:
|
||||
import sys
|
||||
print >> sys.stderr, "'saved_queries' template tag (django-helpdesk) crashed with following error:"
|
||||
print >> sys.stderr, e
|
||||
print >> sys.stderr, "'saved_queries' template tag (django-helpdesk) crashed with following error:"
|
||||
print >> sys.stderr, e
|
||||
return ''
|
||||
|
||||
register = Library()
|
||||
|
@ -20,18 +20,6 @@ from django.utils.safestring import mark_safe
|
||||
from helpdesk.models import Ticket
|
||||
|
||||
|
||||
class ReverseProxy:
|
||||
def __init__(self, sequence):
|
||||
self.sequence = sequence
|
||||
|
||||
def __iter__(self):
|
||||
length = len(self.sequence)
|
||||
i = length
|
||||
while i > 0:
|
||||
i = i - 1
|
||||
yield self.sequence[i]
|
||||
|
||||
|
||||
def num_to_link(text):
|
||||
if text == '':
|
||||
return text
|
||||
@ -40,9 +28,7 @@ def num_to_link(text):
|
||||
for match in re.finditer(r"(?:[^&]|\b|^)#(\d+)\b", text):
|
||||
matches.append(match)
|
||||
|
||||
for match in ReverseProxy(matches):
|
||||
start = match.start()
|
||||
end = match.end()
|
||||
for match in reversed(matches):
|
||||
number = match.groups()[0]
|
||||
url = reverse('helpdesk:view', args=[number])
|
||||
try:
|
||||
@ -52,7 +38,8 @@ def num_to_link(text):
|
||||
|
||||
if ticket:
|
||||
style = ticket.get_status_display()
|
||||
text = "%s <a href='%s' class='ticket_link_status ticket_link_status_%s'>#%s</a>%s" % (text[:match.start()], url, style, match.groups()[0], text[match.end():])
|
||||
text = "%s <a href='%s' class='ticket_link_status ticket_link_status_%s'>#%s</a>%s" % (
|
||||
text[:match.start()], url, style, match.groups()[0], text[match.end():])
|
||||
return mark_safe(text)
|
||||
|
||||
register = template.Library()
|
||||
|
@ -12,6 +12,7 @@ templatetags/admin_url.py - Very simple template tag allow linking to the
|
||||
from django import template
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
|
||||
def user_admin_url(action):
|
||||
user = get_user_model()
|
||||
try:
|
||||
|
@ -1,11 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
try:
|
||||
from django.contrib.auth import get_user_model
|
||||
except ImportError:
|
||||
from django.contrib.auth.models import User
|
||||
else:
|
||||
User = get_user_model()
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def get_staff_user(username='helpdesk.staff', password='password'):
|
||||
|
@ -1,9 +1,9 @@
|
||||
from helpdesk.models import Queue, CustomField, Ticket
|
||||
from helpdesk.models import Queue, Ticket
|
||||
from django.test import TestCase
|
||||
from django.core import mail
|
||||
from django.test.client import Client
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
|
||||
class PublicActionsTestCase(TestCase):
|
||||
"""
|
||||
Tests for public actions:
|
||||
@ -11,17 +11,28 @@ class PublicActionsTestCase(TestCase):
|
||||
- Add a followup
|
||||
- Close resolved case
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create a queue & ticket we can use for later tests.
|
||||
"""
|
||||
self.queue = Queue.objects.create(title='Queue 1', slug='q', allow_public_submission=True, new_ticket_cc='new.public@example.com', updated_ticket_cc='update.public@example.com')
|
||||
self.ticket = Ticket.objects.create(title='Test Ticket', queue=self.queue, submitter_email='test.submitter@example.com', description='This is a test ticket.')
|
||||
self.queue = Queue.objects.create(title='Queue 1',
|
||||
slug='q',
|
||||
allow_public_submission=True,
|
||||
new_ticket_cc='new.public@example.com',
|
||||
updated_ticket_cc='update.public@example.com')
|
||||
self.ticket = Ticket.objects.create(title='Test Ticket',
|
||||
queue=self.queue,
|
||||
submitter_email='test.submitter@example.com',
|
||||
description='This is a test ticket.')
|
||||
|
||||
self.client = Client()
|
||||
|
||||
def test_public_view_ticket(self):
|
||||
response = self.client.get('%s?ticket=%s&email=%s' % (reverse('helpdesk:public_view'), self.ticket.ticket_for_url, 'test.submitter@example.com'))
|
||||
response = self.client.get('%s?ticket=%s&email=%s' % (
|
||||
reverse('helpdesk:public_view'),
|
||||
self.ticket.ticket_for_url,
|
||||
'test.submitter@example.com'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html')
|
||||
|
||||
@ -31,23 +42,26 @@ class PublicActionsTestCase(TestCase):
|
||||
resolution_text = 'Resolved by test script'
|
||||
|
||||
ticket = Ticket.objects.get(id=self.ticket.id)
|
||||
|
||||
|
||||
ticket.status = Ticket.RESOLVED_STATUS
|
||||
ticket.resolution = resolution_text
|
||||
ticket.save()
|
||||
|
||||
current_followups = ticket.followup_set.all().count()
|
||||
|
||||
response = self.client.get('%s?ticket=%s&email=%s&close' % (reverse('helpdesk:public_view'), ticket.ticket_for_url, 'test.submitter@example.com'))
|
||||
|
||||
|
||||
response = self.client.get('%s?ticket=%s&email=%s&close' % (
|
||||
reverse('helpdesk:public_view'),
|
||||
ticket.ticket_for_url,
|
||||
'test.submitter@example.com'))
|
||||
|
||||
ticket = Ticket.objects.get(id=self.ticket.id)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html')
|
||||
self.assertEqual(ticket.status, Ticket.CLOSED_STATUS)
|
||||
self.assertEqual(ticket.resolution, resolution_text)
|
||||
self.assertEqual(current_followups+1, ticket.followup_set.all().count())
|
||||
|
||||
self.assertEqual(current_followups + 1, ticket.followup_set.all().count())
|
||||
|
||||
ticket.resolution = old_resolution
|
||||
ticket.status = old_status
|
||||
ticket.save()
|
||||
|
@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from helpdesk.models import Ticket, Queue
|
||||
from helpdesk.tests.helpers import get_staff_user, reload_urlconf
|
||||
from helpdesk.models import Queue
|
||||
from helpdesk.tests.helpers import get_staff_user
|
||||
|
||||
|
||||
class TestSavingSharedQuery(TestCase):
|
||||
def setUp(self):
|
||||
@ -15,12 +16,15 @@ class TestSavingSharedQuery(TestCase):
|
||||
url = reverse('helpdesk:savequery')
|
||||
self.client.login(username=get_staff_user().get_username(),
|
||||
password='password')
|
||||
response = self.client.post(url,
|
||||
data={'title': 'ticket on my queue',
|
||||
'queue':self.q,
|
||||
'shared':'on',
|
||||
'query_encoded':'KGRwMApWZmlsdGVyaW5nCnAxCihkcDIKVnN0YXR1c19faW4KcDMKKGxwNApJMQphSTIKYUkzCmFzc1Zzb3J0aW5nCnA1ClZjcmVhdGVkCnA2CnMu'})
|
||||
response = self.client.post(
|
||||
url,
|
||||
data={
|
||||
'title': 'ticket on my queue',
|
||||
'queue': self.q,
|
||||
'shared': 'on',
|
||||
'query_encoded':
|
||||
'KGRwMApWZmlsdGVyaW5nCnAxCihkcDIKVnN0YXR1c19faW4KcDMKKG'
|
||||
'xwNApJMQphSTIKYUkzCmFzc1Zzb3J0aW5nCnA1ClZjcmVhdGVkCnA2CnMu'
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue('tickets/?saved_query=1' in response.url)
|
||||
|
||||
|
||||
|
@ -3,6 +3,7 @@ from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from helpdesk.models import Ticket, Queue
|
||||
|
||||
|
||||
class TestKBDisabled(TestCase):
|
||||
def setUp(self):
|
||||
q = Queue(title='Q1', slug='q1')
|
||||
@ -14,22 +15,19 @@ class TestKBDisabled(TestCase):
|
||||
|
||||
def test_ticket_by_id(self):
|
||||
"""Can a ticket be looked up by its ID"""
|
||||
from django.core.urlresolvers import NoReverseMatch
|
||||
|
||||
# get the ticket from models
|
||||
t = Ticket.objects.get(id=self.ticket.id)
|
||||
self.assertEqual(t.title, self.ticket.title)
|
||||
|
||||
def test_ticket_by_link(self):
|
||||
"""Can a ticket be looked up by its link from (eg) an email"""
|
||||
# Work out the link which would have been inserted into the email
|
||||
link = self.ticket.ticket_url
|
||||
# however instead of using that link, we will exercise 'reverse'
|
||||
# to lookup/build the URL from the ticket info we have
|
||||
# http://example.com/helpdesk/view/?ticket=q1-1&email=None
|
||||
# Instead of using the ticket_for_url link,
|
||||
# we will exercise 'reverse' to lookup/build the URL
|
||||
# from the ticket info we have
|
||||
# http://example.com/helpdesk/view/?ticket=q1-1&email=None
|
||||
response = self.client.get(reverse('helpdesk:public_view'),
|
||||
{'ticket': self.ticket.ticket_for_url,
|
||||
'email':self.ticket.submitter_email})
|
||||
'email': self.ticket.submitter_email})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_ticket_with_changed_queue(self):
|
||||
@ -40,7 +38,7 @@ class TestKBDisabled(TestCase):
|
||||
# grab the URL / params which would have been emailed out to submitter.
|
||||
url = reverse('helpdesk:public_view')
|
||||
params = {'ticket': self.ticket.ticket_for_url,
|
||||
'email':self.ticket.submitter_email}
|
||||
'email': self.ticket.submitter_email}
|
||||
# Pickup the ticket created in setup() and change its queue
|
||||
self.ticket.queue = q2
|
||||
self.ticket.save()
|
||||
|
@ -14,13 +14,23 @@ class TicketBasicsTestCase(TestCase):
|
||||
fixtures = ['emailtemplate.json']
|
||||
|
||||
def setUp(self):
|
||||
self.queue_public = Queue.objects.create(title='Queue 1', slug='q1', allow_public_submission=True, new_ticket_cc='new.public@example.com', updated_ticket_cc='update.public@example.com')
|
||||
self.queue_private = Queue.objects.create(title='Queue 2', slug='q2', allow_public_submission=False, new_ticket_cc='new.private@example.com', updated_ticket_cc='update.private@example.com')
|
||||
self.queue_public = Queue.objects.create(
|
||||
title='Queue 1',
|
||||
slug='q1',
|
||||
allow_public_submission=True,
|
||||
new_ticket_cc='new.public@example.com',
|
||||
updated_ticket_cc='update.public@example.com')
|
||||
self.queue_private = Queue.objects.create(
|
||||
title='Queue 2',
|
||||
slug='q2',
|
||||
allow_public_submission=False,
|
||||
new_ticket_cc='new.private@example.com',
|
||||
updated_ticket_cc='update.private@example.com')
|
||||
|
||||
self.ticket_data = {
|
||||
'title': 'Test Ticket',
|
||||
'description': 'Some Test Ticket',
|
||||
}
|
||||
'title': 'Test Ticket',
|
||||
'description': 'Some Test Ticket',
|
||||
}
|
||||
|
||||
self.client = Client()
|
||||
|
||||
@ -30,7 +40,6 @@ class TicketBasicsTestCase(TestCase):
|
||||
ticket = Ticket.objects.create(**ticket_data)
|
||||
self.assertEqual(ticket.ticket_for_url, "q1-%s" % ticket.id)
|
||||
self.assertEqual(email_count, len(mail.outbox))
|
||||
|
||||
|
||||
def test_create_ticket_public(self):
|
||||
email_count = len(mail.outbox)
|
||||
@ -39,17 +48,17 @@ class TicketBasicsTestCase(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
post_data = {
|
||||
'title': 'Test ticket title',
|
||||
'queue': self.queue_public.id,
|
||||
'submitter_email': 'ticket1.submitter@example.com',
|
||||
'body': 'Test ticket body',
|
||||
'priority': 3,
|
||||
}
|
||||
'title': 'Test ticket title',
|
||||
'queue': self.queue_public.id,
|
||||
'submitter_email': 'ticket1.submitter@example.com',
|
||||
'body': 'Test ticket body',
|
||||
'priority': 3,
|
||||
}
|
||||
|
||||
response = self.client.post(reverse('helpdesk:home'), post_data, follow=True)
|
||||
last_redirect = response.redirect_chain[-1]
|
||||
last_redirect_url = last_redirect[0]
|
||||
last_redirect_status = last_redirect[1]
|
||||
# last_redirect_status = last_redirect[1]
|
||||
|
||||
# Ensure we landed on the "View" page.
|
||||
# Django 1.9 compatible way of testing this
|
||||
@ -58,17 +67,17 @@ class TicketBasicsTestCase(TestCase):
|
||||
self.assertEqual(urlparts.path, reverse('helpdesk:public_view'))
|
||||
|
||||
# Ensure submitter, new-queue + update-queue were all emailed.
|
||||
self.assertEqual(email_count+3, len(mail.outbox))
|
||||
self.assertEqual(email_count + 3, len(mail.outbox))
|
||||
|
||||
def test_create_ticket_private(self):
|
||||
email_count = len(mail.outbox)
|
||||
post_data = {
|
||||
'title': 'Private ticket test',
|
||||
'queue': self.queue_private.id,
|
||||
'submitter_email': 'ticket2.submitter@example.com',
|
||||
'body': 'Test ticket body',
|
||||
'priority': 3,
|
||||
}
|
||||
'title': 'Private ticket test',
|
||||
'queue': self.queue_private.id,
|
||||
'submitter_email': 'ticket2.submitter@example.com',
|
||||
'body': 'Test ticket body',
|
||||
'priority': 3,
|
||||
}
|
||||
|
||||
response = self.client.post(reverse('helpdesk:home'), post_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@ -77,24 +86,35 @@ class TicketBasicsTestCase(TestCase):
|
||||
|
||||
def test_create_ticket_customfields(self):
|
||||
email_count = len(mail.outbox)
|
||||
queue_custom = Queue.objects.create(title='Queue 3', slug='q3', allow_public_submission=True, updated_ticket_cc='update.custom@example.com')
|
||||
custom_field_1 = CustomField.objects.create(name='textfield', label='Text Field', data_type='varchar', max_length=100, ordering=10, required=False, staff_only=False)
|
||||
queue_custom = Queue.objects.create(
|
||||
title='Queue 3',
|
||||
slug='q3',
|
||||
allow_public_submission=True,
|
||||
updated_ticket_cc='update.custom@example.com')
|
||||
custom_field_1 = CustomField.objects.create(
|
||||
name='textfield',
|
||||
label='Text Field',
|
||||
data_type='varchar',
|
||||
max_length=100,
|
||||
ordering=10,
|
||||
required=False,
|
||||
staff_only=False)
|
||||
post_data = {
|
||||
'queue': queue_custom.id,
|
||||
'title': 'Ticket with custom text field',
|
||||
'submitter_email': 'ticket3.submitter@example.com',
|
||||
'body': 'Test ticket body',
|
||||
'priority': 3,
|
||||
'custom_textfield': 'This is my custom text.',
|
||||
}
|
||||
'queue': queue_custom.id,
|
||||
'title': 'Ticket with custom text field',
|
||||
'submitter_email': 'ticket3.submitter@example.com',
|
||||
'body': 'Test ticket body',
|
||||
'priority': 3,
|
||||
'custom_textfield': 'This is my custom text.',
|
||||
}
|
||||
|
||||
response = self.client.post(reverse('helpdesk:home'), post_data, follow=True)
|
||||
|
||||
custom_field_1.delete()
|
||||
last_redirect = response.redirect_chain[-1]
|
||||
last_redirect_url = last_redirect[0]
|
||||
last_redirect_status = last_redirect[1]
|
||||
|
||||
# last_redirect_status = last_redirect[1]
|
||||
|
||||
# Ensure we landed on the "View" page.
|
||||
# Django 1.9 compatible way of testing this
|
||||
# https://docs.djangoproject.com/en/1.9/releases/1.9/#http-redirects-no-longer-forced-to-absolute-uris
|
||||
@ -102,4 +122,4 @@ class TicketBasicsTestCase(TestCase):
|
||||
self.assertEqual(urlparts.path, reverse('helpdesk:public_view'))
|
||||
|
||||
# Ensure only two e-mails were sent - submitter & updated.
|
||||
self.assertEqual(email_count+2, len(mail.outbox))
|
||||
self.assertEqual(email_count + 2, len(mail.outbox))
|
||||
|
@ -7,21 +7,18 @@ urls.py - Mapping of URL's to our various views. Note we always used NAMED
|
||||
views for simplicity in linking later on.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
import django
|
||||
if django.get_version().startswith("1.3"):
|
||||
from django.conf.urls.defaults import *
|
||||
else:
|
||||
from django.conf.urls import *
|
||||
from django.conf.urls import url
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth import views as auth_views
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.views import feeds, staff, public, api, kb
|
||||
from django.contrib.auth import views as auth_views
|
||||
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
class DirectTemplateView(TemplateView):
|
||||
extra_context = None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(self.__class__, self).get_context_data(**kwargs)
|
||||
if self.extra_context is not None:
|
||||
|
@ -11,16 +11,10 @@ The API documentation can be accessed by visiting http://helpdesk/api/help/
|
||||
through templates/helpdesk/help_api.html.
|
||||
"""
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth import authenticate
|
||||
try:
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
except ImportError:
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.template import loader, Context
|
||||
import simplejson
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
@ -35,6 +29,8 @@ from helpdesk.models import Ticket, Queue, FollowUp
|
||||
|
||||
import warnings
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
STATUS_OK = 200
|
||||
|
||||
STATUS_ERROR = 400
|
||||
@ -61,7 +57,9 @@ def api(request, method):
|
||||
|
||||
"""
|
||||
|
||||
warnings.warn("django-helpdesk API will be removed in January 2016. See https://github.com/django-helpdesk/django-helpdesk/issues/198 for details.", category=DeprecationWarning)
|
||||
warnings.warn("django-helpdesk API will be removed in January 2016. "
|
||||
"See https://github.com/django-helpdesk/django-helpdesk/issues/198 for details.",
|
||||
category=DeprecationWarning)
|
||||
|
||||
if method == 'help':
|
||||
return render(request, template_name='helpdesk/help_api.html')
|
||||
@ -114,7 +112,6 @@ class API:
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
|
||||
def api_public_create_ticket(self):
|
||||
form = TicketForm(self.request.POST)
|
||||
form.fields['queue'].choices = [[q.id, q.title] for q in Queue.objects.all()]
|
||||
@ -126,10 +123,11 @@ class API:
|
||||
else:
|
||||
return api_return(STATUS_ERROR, text=form.errors.as_text())
|
||||
|
||||
|
||||
def api_public_list_queues(self):
|
||||
return api_return(STATUS_OK, simplejson.dumps([{"id": "%s" % q.id, "title": "%s" % q.title} for q in Queue.objects.all()]), json=True)
|
||||
|
||||
return api_return(STATUS_OK, simplejson.dumps([
|
||||
{"id": "%s" % q.id, "title": "%s" % q.title}
|
||||
for q in Queue.objects.all()
|
||||
]), json=True)
|
||||
|
||||
def api_public_find_user(self):
|
||||
username = self.request.POST.get('username', False)
|
||||
@ -141,7 +139,6 @@ class API:
|
||||
except User.DoesNotExist:
|
||||
return api_return(STATUS_ERROR, "Invalid username provided")
|
||||
|
||||
|
||||
def api_public_delete_ticket(self):
|
||||
if not self.request.POST.get('confirm', False):
|
||||
return api_return(STATUS_ERROR, "No confirmation provided")
|
||||
@ -155,7 +152,6 @@ class API:
|
||||
|
||||
return api_return(STATUS_OK)
|
||||
|
||||
|
||||
def api_public_hold_ticket(self):
|
||||
try:
|
||||
ticket = Ticket.objects.get(id=self.request.POST.get('ticket', False))
|
||||
@ -167,7 +163,6 @@ class API:
|
||||
|
||||
return api_return(STATUS_OK)
|
||||
|
||||
|
||||
def api_public_unhold_ticket(self):
|
||||
try:
|
||||
ticket = Ticket.objects.get(id=self.request.POST.get('ticket', False))
|
||||
@ -179,7 +174,6 @@ class API:
|
||||
|
||||
return api_return(STATUS_OK)
|
||||
|
||||
|
||||
def api_public_add_followup(self):
|
||||
try:
|
||||
ticket = Ticket.objects.get(id=self.request.POST.get('ticket', False))
|
||||
@ -264,7 +258,6 @@ class API:
|
||||
|
||||
return api_return(STATUS_OK)
|
||||
|
||||
|
||||
def api_public_resolve(self):
|
||||
try:
|
||||
ticket = Ticket.objects.get(id=self.request.POST.get('ticket', False))
|
||||
@ -289,7 +282,7 @@ class API:
|
||||
context = safe_template_context(ticket)
|
||||
context['resolution'] = f.comment
|
||||
|
||||
subject = '%s %s (Resolved)' % (ticket.ticket, ticket.title)
|
||||
# subject = '%s %s (Resolved)' % (ticket.ticket, ticket.title)
|
||||
|
||||
messages_sent_to = []
|
||||
|
||||
@ -324,7 +317,12 @@ class API:
|
||||
)
|
||||
messages_sent_to.append(ticket.queue.updated_ticket_cc)
|
||||
|
||||
if ticket.assigned_to and self.request.user != ticket.assigned_to and getattr(ticket.assigned_to.usersettings.settings, 'email_on_ticket_apichange', False) and ticket.assigned_to.email and ticket.assigned_to.email not in messages_sent_to:
|
||||
if ticket.assigned_to and \
|
||||
self.request.user != ticket.assigned_to and \
|
||||
getattr(ticket.assigned_to.usersettings.settings,
|
||||
'email_on_ticket_apichange', False) and \
|
||||
ticket.assigned_to.email and \
|
||||
ticket.assigned_to.email not in messages_sent_to:
|
||||
send_templated_mail(
|
||||
'resolved_resolved',
|
||||
context,
|
||||
|
@ -7,11 +7,7 @@ views/feeds.py - A handful of staff-only RSS feeds to provide ticket details
|
||||
to feed readers or similar software.
|
||||
"""
|
||||
|
||||
try:
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
except ImportError:
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.syndication.views import Feed
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import Q
|
||||
@ -20,6 +16,8 @@ from django.shortcuts import get_object_or_404
|
||||
|
||||
from helpdesk.models import Ticket, FollowUp, Queue
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class OpenTicketsByUser(Feed):
|
||||
title_template = 'helpdesk/rss/ticket_title.html'
|
||||
@ -39,22 +37,22 @@ class OpenTicketsByUser(Feed):
|
||||
return _("Helpdesk: Open Tickets in queue %(queue)s for %(username)s") % {
|
||||
'queue': obj['queue'].title,
|
||||
'username': obj['user'].get_username(),
|
||||
}
|
||||
}
|
||||
else:
|
||||
return _("Helpdesk: Open Tickets for %(username)s") % {
|
||||
'username': obj['user'].get_username(),
|
||||
}
|
||||
}
|
||||
|
||||
def description(self, obj):
|
||||
if obj['queue']:
|
||||
return _("Open and Reopened Tickets in queue %(queue)s for %(username)s") % {
|
||||
'queue': obj['queue'].title,
|
||||
'username': obj['user'].get_username(),
|
||||
}
|
||||
}
|
||||
else:
|
||||
return _("Open and Reopened Tickets for %(username)s") % {
|
||||
'username': obj['user'].get_username(),
|
||||
}
|
||||
}
|
||||
|
||||
def link(self, obj):
|
||||
if obj['queue']:
|
||||
@ -62,28 +60,28 @@ class OpenTicketsByUser(Feed):
|
||||
reverse('helpdesk:list'),
|
||||
obj['user'].id,
|
||||
obj['queue'].id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
return u'%s?assigned_to=%s' % (
|
||||
reverse('helpdesk:list'),
|
||||
obj['user'].id,
|
||||
)
|
||||
)
|
||||
|
||||
def items(self, obj):
|
||||
if obj['queue']:
|
||||
return Ticket.objects.filter(
|
||||
assigned_to=obj['user']
|
||||
).filter(
|
||||
queue=obj['queue']
|
||||
).filter(
|
||||
Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)
|
||||
)
|
||||
assigned_to=obj['user']
|
||||
).filter(
|
||||
queue=obj['queue']
|
||||
).filter(
|
||||
Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)
|
||||
)
|
||||
else:
|
||||
return Ticket.objects.filter(
|
||||
assigned_to=obj['user']
|
||||
).filter(
|
||||
Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)
|
||||
)
|
||||
assigned_to=obj['user']
|
||||
).filter(
|
||||
Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)
|
||||
)
|
||||
|
||||
def item_pubdate(self, item):
|
||||
return item.created
|
||||
@ -101,19 +99,18 @@ class UnassignedTickets(Feed):
|
||||
|
||||
title = _('Helpdesk: Unassigned Tickets')
|
||||
description = _('Unassigned Open and Reopened tickets')
|
||||
link = ''#%s?assigned_to=' % reverse('helpdesk:list')
|
||||
link = '' # '%s?assigned_to=' % reverse('helpdesk:list')
|
||||
|
||||
def items(self, obj):
|
||||
return Ticket.objects.filter(
|
||||
assigned_to__isnull=True
|
||||
).filter(
|
||||
Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)
|
||||
)
|
||||
assigned_to__isnull=True
|
||||
).filter(
|
||||
Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)
|
||||
)
|
||||
|
||||
def item_pubdate(self, item):
|
||||
return item.created
|
||||
|
||||
|
||||
def item_author_name(self, item):
|
||||
if item.assigned_to:
|
||||
return item.assigned_to.get_username()
|
||||
@ -127,7 +124,7 @@ class RecentFollowUps(Feed):
|
||||
|
||||
title = _('Helpdesk: Recent Followups')
|
||||
description = _('Recent FollowUps, such as e-mail replies, comments, attachments and resolutions')
|
||||
link = '/tickets/' # reverse('helpdesk:list')
|
||||
link = '/tickets/' # reverse('helpdesk:list')
|
||||
|
||||
def items(self):
|
||||
return FollowUp.objects.order_by('-date')[:20]
|
||||
@ -143,25 +140,25 @@ class OpenTicketsByQueue(Feed):
|
||||
def title(self, obj):
|
||||
return _('Helpdesk: Open Tickets in queue %(queue)s') % {
|
||||
'queue': obj.title,
|
||||
}
|
||||
}
|
||||
|
||||
def description(self, obj):
|
||||
return _('Open and Reopened Tickets in queue %(queue)s') % {
|
||||
'queue': obj.title,
|
||||
}
|
||||
}
|
||||
|
||||
def link(self, obj):
|
||||
return '%s?queue=%s' % (
|
||||
reverse('helpdesk:list'),
|
||||
obj.id,
|
||||
)
|
||||
)
|
||||
|
||||
def items(self, obj):
|
||||
return Ticket.objects.filter(
|
||||
queue=obj
|
||||
).filter(
|
||||
Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)
|
||||
)
|
||||
queue=obj
|
||||
).filter(
|
||||
Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)
|
||||
)
|
||||
|
||||
def item_pubdate(self, item):
|
||||
return item.created
|
||||
@ -171,4 +168,3 @@ class OpenTicketsByQueue(Feed):
|
||||
return item.assigned_to.get_username()
|
||||
else:
|
||||
return _('Unassigned')
|
||||
|
||||
|
@ -8,12 +8,8 @@ views/kb.py - Public-facing knowledgebase views. The knowledgebase is a
|
||||
resolutions to common problems.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.template import RequestContext
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.models import KBCategory, KBItem
|
||||
@ -22,31 +18,28 @@ from helpdesk.models import KBCategory, KBItem
|
||||
def index(request):
|
||||
category_list = KBCategory.objects.all()
|
||||
# TODO: It'd be great to have a list of most popular items here.
|
||||
return render(request, template_name='helpdesk/kb_index.html',
|
||||
context = {
|
||||
'kb_categories': category_list,
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
})
|
||||
return render(request, 'helpdesk/kb_index.html', {
|
||||
'kb_categories': category_list,
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
})
|
||||
|
||||
|
||||
def category(request, slug):
|
||||
category = get_object_or_404(KBCategory, slug__iexact=slug)
|
||||
items = category.kbitem_set.all()
|
||||
return render(request, template_name='helpdesk/kb_category.html',
|
||||
context = {
|
||||
'category': category,
|
||||
'items': items,
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
})
|
||||
return render(request, 'helpdesk/kb_category.html', {
|
||||
'category': category,
|
||||
'items': items,
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
})
|
||||
|
||||
|
||||
def item(request, item):
|
||||
item = get_object_or_404(KBItem, pk=item)
|
||||
return render(request, template_name='helpdesk/kb_item.html',
|
||||
context = {
|
||||
'item': item,
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
})
|
||||
return render(request, 'helpdesk/kb_item.html', {
|
||||
'item': item,
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
})
|
||||
|
||||
|
||||
def vote(request, item):
|
||||
@ -59,4 +52,3 @@ def vote(request, item):
|
||||
item.save()
|
||||
|
||||
return HttpResponseRedirect(item.get_absolute_url())
|
||||
|
||||
|
@ -6,16 +6,15 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
views/public.py - All public facing views, eg non-staff (no authentication
|
||||
required) views.
|
||||
"""
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseRedirect, Http404, HttpResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.template import loader, Context, RequestContext
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.forms import PublicTicketForm
|
||||
from helpdesk.lib import send_templated_mail, text_is_spam
|
||||
from helpdesk.lib import text_is_spam
|
||||
from helpdesk.models import Ticket, Queue, UserSettings, KBCategory
|
||||
|
||||
|
||||
@ -23,7 +22,9 @@ def homepage(request):
|
||||
if not request.user.is_authenticated() and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT:
|
||||
return HttpResponseRedirect(reverse('helpdesk:login'))
|
||||
|
||||
if (request.user.is_staff or (request.user.is_authenticated() and helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE)):
|
||||
if request.user.is_staff or \
|
||||
(request.user.is_authenticated() and
|
||||
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE):
|
||||
try:
|
||||
if request.user.usersettings.settings.get('login_view_ticketlist', False):
|
||||
return HttpResponseRedirect(reverse('helpdesk:list'))
|
||||
@ -34,18 +35,19 @@ def homepage(request):
|
||||
|
||||
if request.method == 'POST':
|
||||
form = PublicTicketForm(request.POST, request.FILES)
|
||||
form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.filter(allow_public_submission=True)]
|
||||
form.fields['queue'].choices = [('', '--------')] + [
|
||||
(q.id, q.title) for q in Queue.objects.filter(allow_public_submission=True)]
|
||||
if form.is_valid():
|
||||
if text_is_spam(form.cleaned_data['body'], request):
|
||||
# This submission is spam. Let's not save it.
|
||||
return render(request, template_name='helpdesk/public_spam.html')
|
||||
else:
|
||||
ticket = form.save()
|
||||
return HttpResponseRedirect('%s?ticket=%s&email=%s'% (
|
||||
return HttpResponseRedirect('%s?ticket=%s&email=%s' % (
|
||||
reverse('helpdesk:public_view'),
|
||||
ticket.ticket_for_url,
|
||||
ticket.submitter_email)
|
||||
)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
queue = Queue.objects.get(slug=request.GET.get('queue', None))
|
||||
@ -59,34 +61,36 @@ def homepage(request):
|
||||
initial_data['submitter_email'] = request.user.email
|
||||
|
||||
form = PublicTicketForm(initial=initial_data)
|
||||
form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.filter(allow_public_submission=True)]
|
||||
form.fields['queue'].choices = [('', '--------')] + [
|
||||
(q.id, q.title) for q in Queue.objects.filter(allow_public_submission=True)]
|
||||
|
||||
knowledgebase_categories = KBCategory.objects.all()
|
||||
|
||||
return render(request, 'helpdesk/public_homepage.html',
|
||||
{
|
||||
'form': form,
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
'kb_categories': knowledgebase_categories
|
||||
})
|
||||
return render(request, 'helpdesk/public_homepage.html', {
|
||||
'form': form,
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
'kb_categories': knowledgebase_categories
|
||||
})
|
||||
|
||||
|
||||
def view_ticket(request):
|
||||
ticket_req = request.GET.get('ticket', '')
|
||||
ticket = False
|
||||
email = request.GET.get('email', '')
|
||||
error_message = ''
|
||||
|
||||
if ticket_req and email:
|
||||
queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req)
|
||||
try:
|
||||
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email)
|
||||
except:
|
||||
ticket = False
|
||||
except ObjectDoesNotExist:
|
||||
error_message = _('Invalid ticket ID or e-mail address. Please try again.')
|
||||
|
||||
if ticket:
|
||||
|
||||
return render(request, 'helpdesk/public_view_form.html', {
|
||||
'ticket': False,
|
||||
'email': email,
|
||||
'error_message': error_message,
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
})
|
||||
else:
|
||||
if request.user.is_staff:
|
||||
redirect_url = reverse('helpdesk:view', args=[ticket_id])
|
||||
if 'close' in request.GET:
|
||||
@ -102,7 +106,7 @@ def view_ticket(request):
|
||||
'public': 1,
|
||||
'title': ticket.title,
|
||||
'comment': _('Submitter accepted resolution and closed ticket'),
|
||||
}
|
||||
}
|
||||
if ticket.assigned_to:
|
||||
request.POST['owner'] = ticket.assigned_to.id
|
||||
request.GET = {}
|
||||
@ -114,25 +118,16 @@ def view_ticket(request):
|
||||
if helpdesk_settings.HELPDESK_NAVIGATION_ENABLED:
|
||||
redirect_url = reverse('helpdesk:view', args=[ticket_id])
|
||||
|
||||
return render(request, 'helpdesk/public_view_ticket.html',
|
||||
{
|
||||
'ticket': ticket,
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
'next': redirect_url,
|
||||
})
|
||||
return render(request, 'helpdesk/public_view_ticket.html', {
|
||||
'ticket': ticket,
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
'next': redirect_url,
|
||||
})
|
||||
|
||||
return render(request, template_name='helpdesk/public_view_form.html',
|
||||
context = {
|
||||
'ticket': ticket,
|
||||
'email': email,
|
||||
'error_message': error_message,
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
})
|
||||
|
||||
def change_language(request):
|
||||
return_to = ''
|
||||
if 'return_to' in request.GET:
|
||||
return_to = request.GET['return_to']
|
||||
|
||||
return render(request, template_name='helpdesk/public_change_language.html',
|
||||
context = {'next': return_to})
|
||||
return render(request, 'helpdesk/public_change_language.html', {'next': return_to})
|
||||
|
File diff suppressed because it is too large
Load Diff
47
quicktest.py
47
quicktest.py
@ -38,6 +38,27 @@ class QuickDjangoTest(object):
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': (
|
||||
# Defaults:
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.i18n",
|
||||
"django.template.context_processors.media",
|
||||
"django.template.context_processors.static",
|
||||
"django.template.context_processors.tz",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
# Our extra:
|
||||
"django.template.context_processors.request",
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.apps = args
|
||||
# Get the version of the test suite
|
||||
@ -61,11 +82,11 @@ class QuickDjangoTest(object):
|
||||
"""
|
||||
Fire up the Django test suite from before version 1.2
|
||||
"""
|
||||
settings.configure(DEBUG = True,
|
||||
DATABASE_ENGINE = 'sqlite3',
|
||||
DATABASE_NAME = os.path.join(self.DIRNAME, 'database.db'),
|
||||
INSTALLED_APPS = self.INSTALLED_APPS + self.apps
|
||||
)
|
||||
settings.configure(DEBUG=True,
|
||||
DATABASE_ENGINE='sqlite3',
|
||||
DATABASE_NAME=os.path.join(self.DIRNAME, 'database.db'),
|
||||
INSTALLED_APPS=self.INSTALLED_APPS + self.apps
|
||||
)
|
||||
from django.test.simple import run_tests
|
||||
failures = run_tests(self.apps, verbosity=1)
|
||||
if failures:
|
||||
@ -77,8 +98,8 @@ class QuickDjangoTest(object):
|
||||
"""
|
||||
|
||||
settings.configure(
|
||||
DEBUG = True,
|
||||
DATABASES = {
|
||||
DEBUG=True,
|
||||
DATABASES={
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(self.DIRNAME, 'database.db'),
|
||||
@ -88,15 +109,16 @@ class QuickDjangoTest(object):
|
||||
'PORT': '',
|
||||
}
|
||||
},
|
||||
INSTALLED_APPS = self.INSTALLED_APPS + self.apps,
|
||||
MIDDLEWARE_CLASSES = self.MIDDLEWARE_CLASSES,
|
||||
ROOT_URLCONF = 'helpdesk.tests.urls',
|
||||
STATIC_URL = '/static/'
|
||||
INSTALLED_APPS=self.INSTALLED_APPS + self.apps,
|
||||
MIDDLEWARE_CLASSES=self.MIDDLEWARE_CLASSES,
|
||||
ROOT_URLCONF='helpdesk.tests.urls',
|
||||
STATIC_URL='/static/',
|
||||
TEMPLATES=self.TEMPLATES
|
||||
)
|
||||
|
||||
# compatibility with django 1.8 downwards
|
||||
# see: http://stackoverflow.com/questions/3841725/how-to-launch-tests-for-django-reusable-app
|
||||
|
||||
|
||||
try:
|
||||
# Django >= 1.6
|
||||
from django.test.runner import DiscoverRunner
|
||||
@ -129,4 +151,3 @@ if __name__ == '__main__':
|
||||
parser.add_argument('apps', nargs='+', type=str)
|
||||
args = parser.parse_args()
|
||||
QuickDjangoTest(*args.apps)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user