diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..36bd2d5c --- /dev/null +++ b/.coveragerc @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index d0c4e131..f1220a0f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,6 @@ dist django_helpdesk.egg-info docs/html/* docs/doctrees/* +.coverage .project .pydevproject \ No newline at end of file diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..6d986bd6 --- /dev/null +++ b/.pylintrc @@ -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*(# )??$ + +# 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 diff --git a/.travis.yml b/.travis.yml index 1f5b9ac0..60080bd6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 \ No newline at end of file +script: + - coverage run --source='.' quicktest.py helpdesk + +after_success: + - codecov \ No newline at end of file diff --git a/README.rst b/README.rst index 0d770035..299013c7 100644 --- a/README.rst +++ b/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. \ No newline at end of file diff --git a/docs/settings.rst b/docs/settings.rst index ed3f716d..9929d540 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -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 diff --git a/helpdesk/admin.py b/helpdesk/admin.py index 5529f2b6..3ff8c857 100644 --- a/helpdesk/admin.py +++ b/helpdesk/admin.py @@ -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) diff --git a/helpdesk/akismet.py b/helpdesk/akismet.py index b5783678..64100c1b 100644 --- a/helpdesk/akismet.py +++ b/helpdesk/akismet.py @@ -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) diff --git a/helpdesk/apps.py b/helpdesk/apps.py index 8a9755a4..a573b4ca 100644 --- a/helpdesk/apps.py +++ b/helpdesk/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig + class HelpdeskConfig(AppConfig): name = 'helpdesk' verbose_name = "Helpdesk" - diff --git a/helpdesk/forms.py b/helpdesk/forms.py index ecadc515..090d5455 100644 --- a/helpdesk/forms.py +++ b/helpdesk/forms.py @@ -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',) diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 95dddbd0..364abc1e 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -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', '
') 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 diff --git a/helpdesk/management/commands/create_escalation_exclusions.py b/helpdesk/management/commands/create_escalation_exclusions.py index 089e9b26..fcda257d 100644 --- a/helpdesk/management/commands/create_escalation_exclusions.py +++ b/helpdesk/management/commands/create_escalation_exclusions.py @@ -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) diff --git a/helpdesk/management/commands/create_queue_permissions.py b/helpdesk/management/commands/create_queue_permissions.py index 66118e75..50e980c3 100644 --- a/helpdesk/management/commands/create_queue_permissions.py +++ b/helpdesk/management/commands/create_queue_permissions.py @@ -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") - diff --git a/helpdesk/management/commands/create_usersettings.py b/helpdesk/management/commands/create_usersettings.py index eafa6278..46280159 100644 --- a/helpdesk/management/commands/create_usersettings.py +++ b/helpdesk/management/commands/create_usersettings.py @@ -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}) diff --git a/helpdesk/management/commands/escalate_tickets.py b/helpdesk/management/commands/escalate_tickets.py index 9212813b..b2788762 100644 --- a/helpdesk/management/commands/escalate_tickets.py +++ b/helpdesk/management/commands/escalate_tickets.py @@ -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() diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 99f45f73..d40a7958 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -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\d+)\]", subject) + matchobj = re.match(r".*\[" + queue.slug + "-(?P\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() - diff --git a/helpdesk/migrations/0003_initial_data_import.py b/helpdesk/migrations/0003_initial_data_import.py index 566993e0..cc478377 100644 --- a/helpdesk/migrations/0003_initial_data_import.py +++ b/helpdesk/migrations/0003_initial_data_import.py @@ -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() diff --git a/helpdesk/models.py b/helpdesk/models.py index 57823fcc..9405730c 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -12,14 +12,10 @@ from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.db import models -from django.contrib.auth import get_user_model from django.conf import settings from django.utils.translation import ugettext_lazy as _, ugettext -from django import VERSION from django.utils.encoding import python_2_unicode_compatible -from helpdesk import settings as helpdesk_settings - try: from django.utils import timezone except ImportError: @@ -40,56 +36,56 @@ class Queue(models.Model): title = models.CharField( _('Title'), max_length=100, - ) + ) slug = models.SlugField( _('Slug'), max_length=50, unique=True, help_text=_('This slug is used when building ticket ID\'s. Once set, ' - 'try not to change it or e-mailing may get messy.'), - ) + 'try not to change it or e-mailing may get messy.'), + ) email_address = models.EmailField( _('E-Mail Address'), blank=True, null=True, help_text=_('All outgoing e-mails for this queue will use this e-mail ' - 'address. If you use IMAP or POP3, this should be the e-mail ' - 'address for that mailbox.'), - ) + 'address. If you use IMAP or POP3, this should be the e-mail ' + 'address for that mailbox.'), + ) locale = models.CharField( _('Locale'), max_length=10, blank=True, null=True, - help_text=_('Locale of this queue. All correspondence in this queue will be in this language.'), - ) + help_text=_('Locale of this queue. All correspondence in this ' + 'queue will be in this language.'), + ) allow_public_submission = models.BooleanField( _('Allow Public Submission?'), blank=True, default=False, - help_text=_('Should this queue be listed on the public submission ' - 'form?'), - ) + help_text=_('Should this queue be listed on the public submission form?'), + ) allow_email_submission = models.BooleanField( _('Allow E-Mail Submission?'), blank=True, default=False, help_text=_('Do you want to poll the e-mail box below for new ' - 'tickets?'), - ) + 'tickets?'), + ) escalate_days = models.IntegerField( _('Escalation Days'), blank=True, null=True, help_text=_('For tickets which are not held, how often do you wish to ' - 'increase their priority? Set to 0 for no escalation.'), - ) + 'increase their priority? Set to 0 for no escalation.'), + ) new_ticket_cc = models.CharField( _('New Ticket CC Address'), @@ -97,9 +93,9 @@ class Queue(models.Model): null=True, max_length=200, help_text=_('If an e-mail address is entered here, then it will ' - 'receive notification of all new tickets created for this queue. ' - 'Enter a comma between multiple e-mail addresses.'), - ) + 'receive notification of all new tickets created for this queue. ' + 'Enter a comma between multiple e-mail addresses.'), + ) updated_ticket_cc = models.CharField( _('Updated Ticket CC Address'), @@ -107,10 +103,10 @@ class Queue(models.Model): null=True, max_length=200, help_text=_('If an e-mail address is entered here, then it will ' - 'receive notification of all activity (new tickets, closed ' - 'tickets, updates, reassignments, etc) for this queue. Separate ' - 'multiple addresses with a comma.'), - ) + 'receive notification of all activity (new tickets, closed ' + 'tickets, updates, reassignments, etc) for this queue. Separate ' + 'multiple addresses with a comma.'), + ) email_box_type = models.CharField( _('E-Mail Box Type'), @@ -119,8 +115,8 @@ class Queue(models.Model): blank=True, null=True, help_text=_('E-Mail server type for creating tickets automatically ' - 'from a mailbox - both POP3 and IMAP are supported.'), - ) + 'from a mailbox - both POP3 and IMAP are supported.'), + ) email_box_host = models.CharField( _('E-Mail Hostname'), @@ -128,25 +124,25 @@ class Queue(models.Model): blank=True, null=True, help_text=_('Your e-mail server address - either the domain name or ' - 'IP address. May be "localhost".'), - ) + 'IP address. May be "localhost".'), + ) email_box_port = models.IntegerField( _('E-Mail Port'), blank=True, null=True, help_text=_('Port number to use for accessing e-mail. Default for ' - 'POP3 is "110", and for IMAP is "143". This may differ on some ' - 'servers. Leave it blank to use the defaults.'), - ) + 'POP3 is "110", and for IMAP is "143". This may differ on some ' + 'servers. Leave it blank to use the defaults.'), + ) email_box_ssl = models.BooleanField( _('Use SSL for E-Mail?'), blank=True, default=False, help_text=_('Whether to use SSL for IMAP or POP3 - the default ports ' - 'when using SSL are 993 for IMAP and 995 for POP3.'), - ) + 'when using SSL are 993 for IMAP and 995 for POP3.'), + ) email_box_user = models.CharField( _('E-Mail Username'), @@ -154,7 +150,7 @@ class Queue(models.Model): blank=True, null=True, help_text=_('Username for accessing this mailbox.'), - ) + ) email_box_pass = models.CharField( _('E-Mail Password'), @@ -162,7 +158,7 @@ class Queue(models.Model): blank=True, null=True, help_text=_('Password for the above username'), - ) + ) email_box_imap_folder = models.CharField( _('IMAP Folder'), @@ -170,10 +166,10 @@ class Queue(models.Model): blank=True, null=True, help_text=_('If using IMAP, what folder do you wish to fetch messages ' - 'from? This allows you to use one IMAP account for multiple ' - 'queues, by filtering messages on your IMAP server into separate ' - 'folders. Default: INBOX.'), - ) + 'from? This allows you to use one IMAP account for multiple ' + 'queues, by filtering messages on your IMAP server into separate ' + 'folders. Default: INBOX.'), + ) permission_name = models.CharField( _('Django auth permission name'), @@ -182,8 +178,7 @@ class Queue(models.Model): null=True, editable=False, help_text=_('Name used in the django.contrib.auth permission system'), - ) - + ) email_box_interval = models.IntegerField( _('E-Mail Check Interval'), @@ -191,14 +186,14 @@ class Queue(models.Model): blank=True, null=True, default='5', - ) + ) email_box_last_check = models.DateTimeField( blank=True, null=True, editable=False, # This is updated by management/commands/get_mail.py. - ) + ) socks_proxy_type = models.CharField( _('Socks Proxy Type'), @@ -351,32 +346,32 @@ class Ticket(models.Model): title = models.CharField( _('Title'), max_length=200, - ) + ) queue = models.ForeignKey( Queue, verbose_name=_('Queue'), - ) + ) created = models.DateTimeField( _('Created'), blank=True, help_text=_('Date this ticket was first created'), - ) + ) modified = models.DateTimeField( _('Modified'), blank=True, help_text=_('Date this ticket was most recently changed.'), - ) + ) submitter_email = models.EmailField( _('Submitter E-Mail'), blank=True, null=True, help_text=_('The submitter will receive an email for all public ' - 'follow-ups left for this task.'), - ) + 'follow-ups left for this task.'), + ) assigned_to = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -384,35 +379,34 @@ class Ticket(models.Model): blank=True, null=True, verbose_name=_('Assigned to'), - ) + ) status = models.IntegerField( _('Status'), choices=STATUS_CHOICES, default=OPEN_STATUS, - ) + ) on_hold = models.BooleanField( _('On Hold'), blank=True, default=False, - help_text=_('If a ticket is on hold, it will not automatically be ' - 'escalated.'), - ) + help_text=_('If a ticket is on hold, it will not automatically be escalated.'), + ) description = models.TextField( _('Description'), blank=True, null=True, help_text=_('The content of the customers query.'), - ) + ) resolution = models.TextField( _('Resolution'), blank=True, null=True, help_text=_('The resolution provided to the customer by our staff.'), - ) + ) priority = models.IntegerField( _('Priority'), @@ -420,21 +414,21 @@ class Ticket(models.Model): default=3, blank=3, help_text=_('1 = Highest Priority, 5 = Low Priority'), - ) + ) due_date = models.DateTimeField( _('Due on'), blank=True, null=True, - ) + ) last_escalation = models.DateTimeField( blank=True, null=True, editable=False, help_text=_('The date this ticket was last escalated - updated ' - 'automatically by management/commands/escalate_tickets.py.'), - ) + 'automatically by management/commands/escalate_tickets.py.'), + ) def _get_assigned_to(self): """ Custom property to allow us to easily print 'Unassigned' if a @@ -453,7 +447,7 @@ class Ticket(models.Model): """ A user-friendly ticket ID, which is a combination of ticket ID and queue slug. This is generally used in e-mail subjects. """ - return u"[%s]" % (self.ticket_for_url) + return u"[%s]" % self.ticket_for_url ticket = property(_get_ticket) def _get_ticket_for_url(self): @@ -484,9 +478,11 @@ class Ticket(models.Model): Displays the ticket status, with an "On Hold" message if needed. """ held_msg = '' - if self.on_hold: held_msg = _(' - On Hold') + if self.on_hold: + held_msg = _(' - On Hold') dep_msg = '' - if self.can_be_resolved == False: dep_msg = _(' - Open dependencies') + if not self.can_be_resolved: + dep_msg = _(' - Open dependencies') return u'%s%s%s' % (self.get_status_display(), held_msg, dep_msg) get_status = property(_get_status) @@ -506,7 +502,7 @@ class Ticket(models.Model): reverse('helpdesk:public_view'), self.ticket_for_url, self.submitter_email - ) + ) ticket_url = property(_get_ticket_url) def _get_staff_url(self): @@ -523,8 +519,8 @@ class Ticket(models.Model): return u"http://%s%s" % ( site.domain, reverse('helpdesk:view', - args=[self.id]) - ) + args=[self.id]) + ) staff_url = property(_get_staff_url) def _can_be_resolved(self): @@ -534,7 +530,8 @@ class Ticket(models.Model): False = There are non-resolved dependencies """ OPEN_STATUSES = (Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS) - return TicketDependency.objects.filter(ticket=self).filter(depends_on__status__in=OPEN_STATUSES).count() == 0 + return TicketDependency.objects.filter(ticket=self).filter( + depends_on__status__in=OPEN_STATUSES).count() == 0 can_be_resolved = property(_can_be_resolved) class Meta: @@ -547,7 +544,7 @@ class Ticket(models.Model): return '%s %s' % (self.id, self.title) def get_absolute_url(self): - return ('helpdesk:view', (self.id,)) + return 'helpdesk:view', (self.id,) get_absolute_url = models.permalink(get_absolute_url) def save(self, *args, **kwargs): @@ -562,8 +559,8 @@ class Ticket(models.Model): super(Ticket, self).save(*args, **kwargs) - @classmethod - def queue_and_id_from_query(klass, query): + @staticmethod + def queue_and_id_from_query(query): # Apply the opposite logic here compared to self._get_ticket_for_url # Ensure that queues with '-' in them will work parts = query.split('-') @@ -572,6 +569,7 @@ class Ticket(models.Model): class FollowUpManager(models.Manager): + def private_followups(self): return self.filter(public=False) @@ -596,40 +594,40 @@ class FollowUp(models.Model): ticket = models.ForeignKey( Ticket, verbose_name=_('Ticket'), - ) + ) date = models.DateTimeField( _('Date'), - default = timezone.now - ) + default=timezone.now + ) title = models.CharField( _('Title'), max_length=200, blank=True, null=True, - ) + ) comment = models.TextField( _('Comment'), blank=True, null=True, - ) + ) public = models.BooleanField( _('Public'), blank=True, default=False, help_text=_('Public tickets are viewable by the submitter and all ' - 'staff, but non-public tickets can only be seen by staff.'), - ) + 'staff, but non-public tickets can only be seen by staff.'), + ) user = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, verbose_name=_('User'), - ) + ) new_status = models.IntegerField( _('New Status'), @@ -637,12 +635,12 @@ class FollowUp(models.Model): blank=True, null=True, help_text=_('If the status was changed, what was it changed to?'), - ) + ) objects = FollowUpManager() class Meta: - ordering = ['date'] + ordering = ('date',) verbose_name = _('Follow-up') verbose_name_plural = _('Follow-ups') @@ -669,24 +667,24 @@ class TicketChange(models.Model): followup = models.ForeignKey( FollowUp, verbose_name=_('Follow-up'), - ) + ) field = models.CharField( _('Field'), max_length=100, - ) + ) old_value = models.TextField( _('Old Value'), blank=True, null=True, - ) + ) new_value = models.TextField( _('New Value'), blank=True, null=True, - ) + ) def __str__(self): out = '%s ' % self.field @@ -698,7 +696,7 @@ class TicketChange(models.Model): out += ugettext('changed from "%(old_value)s" to "%(new_value)s"') % { 'old_value': self.old_value, 'new_value': self.new_value - } + } return out class Meta: @@ -714,7 +712,7 @@ def attachment_path(instance, filename): import os from django.conf import settings os.umask(0) - path = 'helpdesk/attachments/%s/%s' % (instance.followup.ticket.ticket_for_url, instance.followup.id ) + path = 'helpdesk/attachments/%s/%s' % (instance.followup.ticket.ticket_for_url, instance.followup.id) att_path = os.path.join(settings.MEDIA_ROOT, path) if settings.DEFAULT_FILE_STORAGE == "django.core.files.storage.FileSystemStorage": if not os.path.exists(att_path): @@ -732,28 +730,28 @@ class Attachment(models.Model): followup = models.ForeignKey( FollowUp, verbose_name=_('Follow-up'), - ) + ) file = models.FileField( _('File'), upload_to=attachment_path, max_length=1000, - ) + ) filename = models.CharField( _('Filename'), max_length=1000, - ) + ) mime_type = models.CharField( _('MIME Type'), max_length=255, - ) + ) size = models.IntegerField( _('Size'), help_text=_('Size of this file in bytes'), - ) + ) def get_upload_to(self, field_attname): """ Get upload_to path specific to this item """ @@ -762,13 +760,13 @@ class Attachment(models.Model): return u'helpdesk/attachments/%s/%s' % ( self.followup.ticket.ticket_for_url, self.followup.id - ) + ) def __str__(self): return '%s' % self.filename class Meta: - ordering = ['filename',] + ordering = ('filename',) verbose_name = _('Attachment') verbose_name_plural = _('Attachments') @@ -785,32 +783,31 @@ class PreSetReply(models.Model): When replying to a ticket, the user can select any reply set for the current queue, and the body text is fetched via AJAX. """ + class Meta: + ordering = ('name',) + verbose_name = _('Pre-set reply') + verbose_name_plural = _('Pre-set replies') queues = models.ManyToManyField( Queue, blank=True, help_text=_('Leave blank to allow this reply to be used for all ' - 'queues, or select those queues you wish to limit this reply to.'), - ) + 'queues, or select those queues you wish to limit this reply to.'), + ) name = models.CharField( _('Name'), max_length=100, help_text=_('Only used to assist users with selecting a reply - not ' - 'shown to the user.'), - ) + 'shown to the user.'), + ) body = models.TextField( _('Body'), help_text=_('Context available: {{ ticket }} - ticket object (eg ' - '{{ ticket.title }}); {{ queue }} - The queue; and {{ user }} ' - '- the current user.'), - ) - - class Meta: - ordering = ['name',] - verbose_name = _('Pre-set reply') - verbose_name_plural = _('Pre-set replies') + '{{ ticket.title }}); {{ queue }} - The queue; and {{ user }} ' + '- the current user.'), + ) def __str__(self): return '%s' % self.name @@ -831,20 +828,19 @@ class EscalationExclusion(models.Model): queues = models.ManyToManyField( Queue, blank=True, - help_text=_('Leave blank for this exclusion to be applied to all ' - 'queues, or select those queues you wish to exclude with this ' - 'entry.'), - ) + help_text=_('Leave blank for this exclusion to be applied to all queues, ' + 'or select those queues you wish to exclude with this entry.'), + ) name = models.CharField( _('Name'), max_length=100, - ) + ) date = models.DateField( _('Date'), help_text=_('Date on which escalation should not happen'), - ) + ) def __str__(self): return '%s' % self.name @@ -867,36 +863,35 @@ class EmailTemplate(models.Model): template_name = models.CharField( _('Template Name'), max_length=100, - ) + ) subject = models.CharField( _('Subject'), max_length=100, help_text=_('This will be prefixed with "[ticket.ticket] ticket.title"' - '. We recommend something simple such as "(Updated") or "(Closed)"' - ' - the same context is available as in plain_text, below.'), - ) + '. We recommend something simple such as "(Updated") or "(Closed)"' + ' - the same context is available as in plain_text, below.'), + ) heading = models.CharField( _('Heading'), max_length=100, help_text=_('In HTML e-mails, this will be the heading at the top of ' - 'the email - the same context is available as in plain_text, ' - 'below.'), - ) + 'the email - the same context is available as in plain_text, ' + 'below.'), + ) plain_text = models.TextField( _('Plain Text'), help_text=_('The context available to you includes {{ ticket }}, ' - '{{ queue }}, and depending on the time of the call: ' - '{{ resolution }} or {{ comment }}.'), - ) + '{{ queue }}, and depending on the time of the call: ' + '{{ resolution }} or {{ comment }}.'), + ) html = models.TextField( _('HTML'), - help_text=_('The same context is available here as in plain_text, ' - 'above.'), - ) + help_text=_('The same context is available here as in plain_text, above.'), + ) locale = models.CharField( _('Locale'), @@ -904,13 +899,13 @@ class EmailTemplate(models.Model): blank=True, null=True, help_text=_('Locale of this template.'), - ) + ) def __str__(self): return '%s' % self.template_name class Meta: - ordering = ['template_name', 'locale'] + ordering = ('template_name', 'locale') verbose_name = _('e-mail template') verbose_name_plural = _('e-mail templates') @@ -925,26 +920,26 @@ class KBCategory(models.Model): title = models.CharField( _('Title'), max_length=100, - ) + ) slug = models.SlugField( _('Slug'), - ) + ) description = models.TextField( _('Description'), - ) + ) def __str__(self): return '%s' % self.title class Meta: - ordering = ['title',] + ordering = ('title',) verbose_name = _('Knowledge base category') verbose_name_plural = _('Knowledge base categories') def get_absolute_url(self): - return ('kb_category', (), {'slug': self.slug}) + return 'kb_category', (), {'slug': self.slug} get_absolute_url = models.permalink(get_absolute_url) @@ -957,39 +952,38 @@ class KBItem(models.Model): category = models.ForeignKey( KBCategory, verbose_name=_('Category'), - ) + ) title = models.CharField( _('Title'), max_length=100, - ) + ) question = models.TextField( _('Question'), - ) + ) answer = models.TextField( _('Answer'), - ) + ) votes = models.IntegerField( _('Votes'), help_text=_('Total number of votes cast for this item'), default=0, - ) + ) recommendations = models.IntegerField( _('Positive Votes'), help_text=_('Number of votes for this item which were POSITIVE.'), default=0, - ) + ) last_updated = models.DateTimeField( _('Last Updated'), - help_text=_('The date on which this question was most recently ' - 'changed.'), + help_text=_('The date on which this question was most recently changed.'), blank=True, - ) + ) def save(self, *args, **kwargs): if not self.last_updated: @@ -1007,12 +1001,12 @@ class KBItem(models.Model): return '%s' % self.title class Meta: - ordering = ['title',] + ordering = ('title',) verbose_name = _('Knowledge base item') verbose_name_plural = _('Knowledge base items') def get_absolute_url(self): - return ('helpdesk:kb_item', (self.id,)) + return 'helpdesk:kb_item', (self.id,) get_absolute_url = models.permalink(get_absolute_url) @@ -1031,25 +1025,25 @@ class SavedSearch(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('User'), - ) + ) title = models.CharField( _('Query Name'), max_length=100, help_text=_('User-provided name for this query'), - ) + ) shared = models.BooleanField( _('Shared With Other Users?'), blank=True, default=False, help_text=_('Should other users see this query?'), - ) + ) query = models.TextField( _('Search Query'), help_text=_('Pickled query object. Be wary changing this.'), - ) + ) def __str__(self): if self.shared: @@ -1076,10 +1070,11 @@ class UserSettings(models.Model): settings_pickled = models.TextField( _('Settings Dictionary'), - help_text=_('This is a base64-encoded representation of a pickled Python dictionary. Do not change this field via the admin.'), + help_text=_('This is a base64-encoded representation of a pickled Python dictionary. ' + 'Do not change this field via the admin.'), blank=True, null=True, - ) + ) def _set_settings(self, data): # data should always be a Python dictionary. @@ -1125,15 +1120,7 @@ def create_usersettings(sender, instance, created, **kwargs): if created: UserSettings.objects.create(user=instance, settings=DEFAULT_USER_SETTINGS) -try: - # Connecting via settings.AUTH_USER_MODEL (string) fails in Django < 1.7. We need the actual model there. - # https://docs.djangoproject.com/en/1.7/topics/auth/customizing/#referencing-the-user-model - if VERSION < (1, 7): - raise ValueError - models.signals.post_save.connect(create_usersettings, sender=settings.AUTH_USER_MODEL) -except: - signal_user = get_user_model() - models.signals.post_save.connect(create_usersettings, sender=signal_user) +models.signals.post_save.connect(create_usersettings, sender=settings.AUTH_USER_MODEL) @python_2_unicode_compatible @@ -1143,41 +1130,43 @@ class IgnoreEmail(models.Model): processing IMAP and POP3 mailboxes, eg mails from postmaster or from known trouble-makers. """ + class Meta: + verbose_name = _('Ignored e-mail address') + verbose_name_plural = _('Ignored e-mail addresses') + queues = models.ManyToManyField( Queue, blank=True, - help_text=_('Leave blank for this e-mail to be ignored on all ' - 'queues, or select those queues you wish to ignore this e-mail ' - 'for.'), - ) + help_text=_('Leave blank for this e-mail to be ignored on all queues, ' + 'or select those queues you wish to ignore this e-mail for.'), + ) name = models.CharField( _('Name'), max_length=100, - ) + ) date = models.DateField( _('Date'), help_text=_('Date on which this e-mail address was added'), blank=True, editable=False - ) + ) email_address = models.CharField( _('E-Mail Address'), max_length=150, help_text=_('Enter a full e-mail address, or portions with ' - 'wildcards, eg *@domain.com or postmaster@*.'), - ) + 'wildcards, eg *@domain.com or postmaster@*.'), + ) keep_in_mailbox = models.BooleanField( _('Save Emails in Mailbox?'), blank=True, default=False, - help_text=_('Do you want to save emails from this address in the ' - 'mailbox? If this is unticked, emails from this address will ' - 'be deleted.'), - ) + help_text=_('Do you want to save emails from this address in the mailbox? ' + 'If this is unticked, emails from this address will be deleted.'), + ) def __str__(self): return '%s' % self.name @@ -1202,18 +1191,14 @@ class IgnoreEmail(models.Model): own_parts = self.email_address.split("@") email_parts = email.split("@") - if self.email_address == email \ - or own_parts[0] == "*" and own_parts[1] == email_parts[1] \ - or own_parts[1] == "*" and own_parts[0] == email_parts[0] \ - or own_parts[0] == "*" and own_parts[1] == "*": + if self.email_address == email or \ + own_parts[0] == "*" and own_parts[1] == email_parts[1] or \ + own_parts[1] == "*" and own_parts[0] == email_parts[0] or \ + own_parts[0] == "*" and own_parts[1] == "*": return True else: return False - class Meta: - verbose_name = _('Ignored e-mail address') - verbose_name_plural = _('Ignored e-mail addresses') - @python_2_unicode_compatible class TicketCC(models.Model): @@ -1229,7 +1214,7 @@ class TicketCC(models.Model): ticket = models.ForeignKey( Ticket, verbose_name=_('Ticket'), - ) + ) user = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -1237,28 +1222,28 @@ class TicketCC(models.Model): null=True, help_text=_('User who wishes to receive updates for this ticket.'), verbose_name=_('User'), - ) + ) email = models.EmailField( _('E-Mail Address'), blank=True, null=True, help_text=_('For non-user followers, enter their e-mail address'), - ) + ) can_view = models.BooleanField( _('Can View Ticket?'), blank=True, default=False, help_text=_('Can this CC login to view the ticket details?'), - ) + ) can_update = models.BooleanField( _('Can Update Ticket?'), blank=True, default=False, help_text=_('Can this CC login and update the ticket?'), - ) + ) def _email_address(self): if self.user and self.user.email is not None: @@ -1277,7 +1262,9 @@ class TicketCC(models.Model): def __str__(self): return '%s for %s' % (self.display, self.ticket.title) + class CustomFieldManager(models.Manager): + def get_queryset(self): return super(CustomFieldManager, self).get_queryset().order_by('ordering') @@ -1290,78 +1277,80 @@ class CustomField(models.Model): name = models.SlugField( _('Field Name'), - help_text=_('As used in the database and behind the scenes. Must be unique and consist of only lowercase letters with no punctuation.'), + help_text=_('As used in the database and behind the scenes. ' + 'Must be unique and consist of only lowercase letters with no punctuation.'), unique=True, - ) + ) label = models.CharField( _('Label'), max_length=30, help_text=_('The display label for this field'), - ) + ) help_text = models.TextField( _('Help Text'), help_text=_('Shown to the user when editing the ticket'), blank=True, null=True - ) + ) DATA_TYPE_CHOICES = ( - ('varchar', _('Character (single line)')), - ('text', _('Text (multi-line)')), - ('integer', _('Integer')), - ('decimal', _('Decimal')), - ('list', _('List')), - ('boolean', _('Boolean (checkbox yes/no)')), - ('date', _('Date')), - ('time', _('Time')), - ('datetime', _('Date & Time')), - ('email', _('E-Mail Address')), - ('url', _('URL')), - ('ipaddress', _('IP Address')), - ('slug', _('Slug')), - ) + ('varchar', _('Character (single line)')), + ('text', _('Text (multi-line)')), + ('integer', _('Integer')), + ('decimal', _('Decimal')), + ('list', _('List')), + ('boolean', _('Boolean (checkbox yes/no)')), + ('date', _('Date')), + ('time', _('Time')), + ('datetime', _('Date & Time')), + ('email', _('E-Mail Address')), + ('url', _('URL')), + ('ipaddress', _('IP Address')), + ('slug', _('Slug')), + ) data_type = models.CharField( _('Data Type'), max_length=100, help_text=_('Allows you to restrict the data entered into this field'), choices=DATA_TYPE_CHOICES, - ) + ) max_length = models.IntegerField( _('Maximum Length (characters)'), blank=True, null=True, - ) + ) decimal_places = models.IntegerField( _('Decimal Places'), help_text=_('Only used for decimal fields'), blank=True, null=True, - ) + ) empty_selection_list = models.BooleanField( _('Add empty first choice to List?'), default=False, - help_text=_('Only for List: adds an empty first entry to the choices list, which enforces that the user makes an active choice.'), - ) + help_text=_('Only for List: adds an empty first entry to the choices list, ' + 'which enforces that the user makes an active choice.'), + ) list_values = models.TextField( _('List Values'), help_text=_('For list fields only. Enter one option per line.'), blank=True, null=True, - ) + ) ordering = models.IntegerField( _('Ordering'), help_text=_('Lower numbers are displayed first; higher numbers are listed later'), blank=True, null=True, - ) + ) def _choices_as_array(self): from StringIO import StringIO @@ -1375,18 +1364,19 @@ class CustomField(models.Model): _('Required?'), help_text=_('Does the user have to enter a value for this field?'), default=False, - ) + ) staff_only = models.BooleanField( _('Staff Only?'), - help_text=_('If this is ticked, then the public submission form will NOT show this field'), + help_text=_('If this is ticked, then the public submission form ' + 'will NOT show this field'), default=False, - ) + ) objects = CustomFieldManager() def __str__(self): - return '%s' % (self.name) + return '%s' % self.name class Meta: verbose_name = _('Custom field') @@ -1398,12 +1388,12 @@ class TicketCustomFieldValue(models.Model): ticket = models.ForeignKey( Ticket, verbose_name=_('Ticket'), - ) + ) field = models.ForeignKey( CustomField, verbose_name=_('Field'), - ) + ) value = models.TextField(blank=True, null=True) @@ -1423,22 +1413,22 @@ class TicketDependency(models.Model): To help enforce this, a helper function `can_be_resolved` on each Ticket instance checks that these have all been resolved. """ + class Meta: + unique_together = (('ticket', 'depends_on'),) + verbose_name = _('Ticket dependency') + verbose_name_plural = _('Ticket dependencies') + ticket = models.ForeignKey( Ticket, verbose_name=_('Ticket'), related_name='ticketdependency', - ) + ) depends_on = models.ForeignKey( Ticket, verbose_name=_('Depends On Ticket'), related_name='depends_on', - ) + ) def __str__(self): return '%s / %s' % (self.ticket, self.depends_on) - - class Meta: - unique_together = (('ticket', 'depends_on'),) - verbose_name = _('Ticket dependency') - verbose_name_plural = _('Ticket dependencies') diff --git a/helpdesk/settings.py b/helpdesk/settings.py index 0ead588d..606af04f 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -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) diff --git a/helpdesk/templatetags/in_list.py b/helpdesk/templatetags/in_list.py index 16c3d678..fcc58898 100644 --- a/helpdesk/templatetags/in_list.py +++ b/helpdesk/templatetags/in_list.py @@ -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) diff --git a/helpdesk/templatetags/load_helpdesk_settings.py b/helpdesk/templatetags/load_helpdesk_settings.py index f14e4b30..4b1afcf4 100644 --- a/helpdesk/templatetags/load_helpdesk_settings.py +++ b/helpdesk/templatetags/load_helpdesk_settings.py @@ -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() diff --git a/helpdesk/templatetags/saved_queries.py b/helpdesk/templatetags/saved_queries.py index ebedf9bb..e3f9a954 100644 --- a/helpdesk/templatetags/saved_queries.py +++ b/helpdesk/templatetags/saved_queries.py @@ -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() diff --git a/helpdesk/templatetags/ticket_to_link.py b/helpdesk/templatetags/ticket_to_link.py index 91faafba..1280f846 100644 --- a/helpdesk/templatetags/ticket_to_link.py +++ b/helpdesk/templatetags/ticket_to_link.py @@ -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 #%s%s" % (text[:match.start()], url, style, match.groups()[0], text[match.end():]) + text = "%s #%s%s" % ( + text[:match.start()], url, style, match.groups()[0], text[match.end():]) return mark_safe(text) register = template.Library() diff --git a/helpdesk/templatetags/user_admin_url.py b/helpdesk/templatetags/user_admin_url.py index 13517609..e779ee9f 100644 --- a/helpdesk/templatetags/user_admin_url.py +++ b/helpdesk/templatetags/user_admin_url.py @@ -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: diff --git a/helpdesk/tests/helpers.py b/helpdesk/tests/helpers.py index b64970be..09055adc 100644 --- a/helpdesk/tests/helpers.py +++ b/helpdesk/tests/helpers.py @@ -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'): diff --git a/helpdesk/tests/test_public_actions.py b/helpdesk/tests/test_public_actions.py index 85deefdc..38adc422 100644 --- a/helpdesk/tests/test_public_actions.py +++ b/helpdesk/tests/test_public_actions.py @@ -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() diff --git a/helpdesk/tests/test_savequery.py b/helpdesk/tests/test_savequery.py index 1c0244f0..5aaa7086 100644 --- a/helpdesk/tests/test_savequery.py +++ b/helpdesk/tests/test_savequery.py @@ -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) - - diff --git a/helpdesk/tests/test_ticket_lookup.py b/helpdesk/tests/test_ticket_lookup.py index 4fa30e38..d98406a1 100644 --- a/helpdesk/tests/test_ticket_lookup.py +++ b/helpdesk/tests/test_ticket_lookup.py @@ -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() diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index 7334a129..a88c7121 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -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)) diff --git a/helpdesk/urls.py b/helpdesk/urls.py index 7fd7e009..833a89df 100644 --- a/helpdesk/urls.py +++ b/helpdesk/urls.py @@ -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: diff --git a/helpdesk/views/api.py b/helpdesk/views/api.py index 072caee2..dc286202 100644 --- a/helpdesk/views/api.py +++ b/helpdesk/views/api.py @@ -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, diff --git a/helpdesk/views/feeds.py b/helpdesk/views/feeds.py index cd3bc06c..08fc74ef 100644 --- a/helpdesk/views/feeds.py +++ b/helpdesk/views/feeds.py @@ -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') - diff --git a/helpdesk/views/kb.py b/helpdesk/views/kb.py index 384efd84..a7e7a1e8 100644 --- a/helpdesk/views/kb.py +++ b/helpdesk/views/kb.py @@ -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()) - diff --git a/helpdesk/views/public.py b/helpdesk/views/public.py index d9041e9d..383f36f1 100644 --- a/helpdesk/views/public.py +++ b/helpdesk/views/public.py @@ -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}) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index c96be858..357fa64e 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -7,19 +7,12 @@ views/staff.py - The bulk of the application - provides most business logic and renders all staff-facing views. """ from __future__ import unicode_literals -from django.utils.encoding import python_2_unicode_compatible from datetime import datetime, timedelta -import sys from django import VERSION from django.conf import settings -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.decorators import login_required, user_passes_test -from django.core.files.base import ContentFile +from django.contrib.auth import get_user_model +from django.contrib.auth.decorators import user_passes_test from django.core.urlresolvers import reverse from django.core.exceptions import ValidationError, PermissionDenied from django.core import paginator @@ -27,7 +20,6 @@ from django.db import connection from django.db.models import Q 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.utils.dates import MONTHS_3 from django.utils.translation import ugettext as _ from django.utils.html import escape @@ -38,19 +30,33 @@ try: except ImportError: from datetime import datetime as timezone -from helpdesk.forms import TicketForm, UserSettingsForm, EmailIgnoreForm, EditTicketForm, TicketCCForm, EditFollowUpForm, TicketDependencyForm -from helpdesk.lib import send_templated_mail, query_to_dict, apply_query, safe_template_context -from helpdesk.models import Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment, SavedSearch, IgnoreEmail, TicketCC, TicketDependency +from helpdesk.forms import ( + TicketForm, UserSettingsForm, EmailIgnoreForm, EditTicketForm, TicketCCForm, + EditFollowUpForm, TicketDependencyForm +) +from helpdesk.lib import ( + send_templated_mail, query_to_dict, apply_query, safe_template_context, +) +from helpdesk.models import ( + Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment, SavedSearch, + IgnoreEmail, TicketCC, TicketDependency, +) from helpdesk import settings as helpdesk_settings +User = get_user_model() + + if helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE: # treat 'normal' users like 'staff' - staff_member_required = user_passes_test(lambda u: u.is_authenticated() and u.is_active) + staff_member_required = user_passes_test( + lambda u: u.is_authenticated() and u.is_active) else: - staff_member_required = user_passes_test(lambda u: u.is_authenticated() and u.is_active and u.is_staff) + staff_member_required = user_passes_test( + lambda u: u.is_authenticated() and u.is_active and u.is_staff) -superuser_required = user_passes_test(lambda u: u.is_authenticated() and u.is_active and u.is_superuser) +superuser_required = user_passes_test( + lambda u: u.is_authenticated() and u.is_active and u.is_superuser) def _get_user_queues(user): @@ -60,7 +66,9 @@ def _get_user_queues(user): :return: A Python list of Queues """ all_queues = Queue.objects.all() - limit_queues_by_user = helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION and not user.is_superuser + limit_queues_by_user = \ + helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION \ + and not user.is_superuser if limit_queues_by_user: id_list = [q.pk for q in all_queues if user.has_perm(q.permission_name)] return all_queues.filter(pk__in=id_list) @@ -90,24 +98,24 @@ def dashboard(request): # open & reopened tickets, assigned to current user tickets = Ticket.objects.select_related('queue').filter( - assigned_to=request.user, - ).exclude( - status__in = [Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS], - ) + assigned_to=request.user, + ).exclude( + status__in=[Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS], + ) # closed & resolved tickets, assigned to current user - tickets_closed_resolved = Ticket.objects.select_related('queue').filter( - assigned_to=request.user, - status__in = [Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS]) + tickets_closed_resolved = Ticket.objects.select_related('queue').filter( + assigned_to=request.user, + status__in=[Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS]) user_queues = _get_user_queues(request.user) unassigned_tickets = Ticket.objects.select_related('queue').filter( - assigned_to__isnull=True, - queue__in=user_queues - ).exclude( - status=Ticket.CLOSED_STATUS, - ) + assigned_to__isnull=True, + queue__in=user_queues + ).exclude( + status=Ticket.CLOSED_STATUS, + ) # all tickets, reported by current user all_tickets_reported_by_current_user = '' @@ -117,10 +125,10 @@ def dashboard(request): submitter_email=email_current_user, ).order_by('status') - Tickets = Ticket.objects.filter( - queue__in=user_queues, - ) - basic_ticket_stats = calc_basic_ticket_stats(Tickets) + tickets_in_queues = Ticket.objects.filter( + queue__in=user_queues, + ) + basic_ticket_stats = calc_basic_ticket_stats(tickets_in_queues) # The following query builds a grid of queues & ticket statuses, # to be displayed to the user. EG: @@ -153,15 +161,14 @@ def dashboard(request): dash_tickets = query_to_dict(cursor.fetchall(), cursor.description) - return render(request, 'helpdesk/dashboard.html', - { - 'user_tickets': tickets, - 'user_tickets_closed_resolved': tickets_closed_resolved, - 'unassigned_tickets': unassigned_tickets, - 'all_tickets_reported_by_current_user': all_tickets_reported_by_current_user, - 'dash_tickets': dash_tickets, - 'basic_ticket_stats': basic_ticket_stats, - }) + return render(request, 'helpdesk/dashboard.html', { + 'user_tickets': tickets, + 'user_tickets_closed_resolved': tickets_closed_resolved, + 'unassigned_tickets': unassigned_tickets, + 'all_tickets_reported_by_current_user': all_tickets_reported_by_current_user, + 'dash_tickets': dash_tickets, + 'basic_ticket_stats': basic_ticket_stats, + }) dashboard = staff_member_required(dashboard) @@ -171,38 +178,38 @@ def delete_ticket(request, ticket_id): raise PermissionDenied() if request.method == 'GET': - return render(request, template_name='helpdesk/delete_ticket.html', - context = { - 'ticket': ticket, - }) + return render(request, 'helpdesk/delete_ticket.html', { + 'ticket': ticket, + }) else: ticket.delete() return HttpResponseRedirect(reverse('helpdesk:home')) delete_ticket = staff_member_required(delete_ticket) + def followup_edit(request, ticket_id, followup_id): - "Edit followup options with an ability to change the ticket." + """Edit followup options with an ability to change the ticket.""" followup = get_object_or_404(FollowUp, id=followup_id) ticket = get_object_or_404(Ticket, id=ticket_id) if not _has_access_to_queue(request.user, ticket.queue): raise PermissionDenied() if request.method == 'GET': - form = EditFollowUpForm(initial= - {'title': escape(followup.title), - 'ticket': followup.ticket, - 'comment': escape(followup.comment), - 'public': followup.public, - 'new_status': followup.new_status, - }) + form = EditFollowUpForm(initial={ + 'title': escape(followup.title), + 'ticket': followup.ticket, + 'comment': escape(followup.comment), + 'public': followup.public, + 'new_status': followup.new_status, + }) - ticketcc_string, SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe(request.user, ticket) + ticketcc_string, show_subscribe = \ + return_ticketccstring_and_show_subscribe(request.user, ticket) - return render(request, template_name='helpdesk/followup_edit.html', - context = { - 'followup': followup, - 'ticket': ticket, - 'form': form, - 'ticketcc_string': ticketcc_string, + return render(request, 'helpdesk/followup_edit.html', { + 'followup': followup, + 'ticket': ticket, + 'form': form, + 'ticketcc_string': ticketcc_string, }) elif request.method == 'POST': form = EditFollowUpForm(request.POST) @@ -212,7 +219,7 @@ def followup_edit(request, ticket_id, followup_id): comment = form.cleaned_data['comment'] public = form.cleaned_data['public'] new_status = form.cleaned_data['new_status'] - #will save previous date + # will save previous date old_date = followup.date new_followup = FollowUp(title=title, date=old_date, ticket=_ticket, comment=comment, public=public, new_status=new_status, ) # keep old user if one did exist before. @@ -220,7 +227,7 @@ def followup_edit(request, ticket_id, followup_id): new_followup.user = followup.user new_followup.save() # get list of old attachments & link them to new_followup - attachments = Attachment.objects.filter(followup = followup) + attachments = Attachment.objects.filter(followup=followup) for attachment in attachments: attachment.followup = new_followup attachment.save() @@ -229,8 +236,9 @@ def followup_edit(request, ticket_id, followup_id): return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket.id])) followup_edit = staff_member_required(followup_edit) + def followup_delete(request, ticket_id, followup_id): - ''' followup delete for superuser''' + """followup delete for superuser""" ticket = get_object_or_404(Ticket, id=ticket_id) if not request.user.is_superuser: @@ -262,8 +270,9 @@ def view_ticket(request, ticket_id): if 'subscribe' in request.GET: # Allow the user to subscribe him/herself to the ticket whilst viewing it. - ticketcc_string, SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe(request.user, ticket) - if SHOW_SUBSCRIBE: + ticket_cc, show_subscribe = \ + return_ticketccstring_and_show_subscribe(request.user, ticket) + if show_subscribe: subscribe_staff_member_to_ticket(ticket, request.user) return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket.id])) @@ -281,7 +290,7 @@ def view_ticket(request, ticket_id): 'owner': owner, 'title': ticket.title, 'comment': _('Accepted resolution and closed ticket'), - } + } return update_ticket(request, ticket_id) @@ -290,26 +299,27 @@ def view_ticket(request, ticket_id): else: users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD) - # TODO: shouldn't this template get a form to begin with? - form = TicketForm(initial={'due_date':ticket.due_date}) + form = TicketForm(initial={'due_date': ticket.due_date}) - ticketcc_string, SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe(request.user, ticket) + ticketcc_string, show_subscribe = \ + return_ticketccstring_and_show_subscribe(request.user, ticket) - return render(request, template_name='helpdesk/ticket.html', - context = { - 'ticket': ticket, - 'form': form, - 'active_users': users, - 'priorities': Ticket.PRIORITY_CHOICES, - 'preset_replies': PreSetReply.objects.filter(Q(queues=ticket.queue) | Q(queues__isnull=True)), - 'ticketcc_string': ticketcc_string, - 'SHOW_SUBSCRIBE': SHOW_SUBSCRIBE, - }) + return render(request, 'helpdesk/ticket.html', { + 'ticket': ticket, + 'form': form, + 'active_users': users, + 'priorities': Ticket.PRIORITY_CHOICES, + 'preset_replies': PreSetReply.objects.filter( + Q(queues=ticket.queue) | Q(queues__isnull=True)), + 'ticketcc_string': ticketcc_string, + 'SHOW_SUBSCRIBE': show_subscribe, + }) view_ticket = staff_member_required(view_ticket) + def return_ticketccstring_and_show_subscribe(user, ticket): - ''' used in view_ticket() and followup_edit()''' + """used in view_ticket() and followup_edit()""" # create the ticketcc_string and check whether current user is already # subscribed username = user.get_username().upper() @@ -321,14 +331,14 @@ def return_ticketccstring_and_show_subscribe(user, ticket): ticketcc_string = '' all_ticketcc = ticket.ticketcc_set.all() counter_all_ticketcc = len(all_ticketcc) - 1 - SHOW_SUBSCRIBE = True + show_subscribe = True for i, ticketcc in enumerate(all_ticketcc): ticketcc_this_entry = str(ticketcc.display) - ticketcc_string = ticketcc_string + ticketcc_this_entry + ticketcc_string += ticketcc_this_entry if i < counter_all_ticketcc: - ticketcc_string = ticketcc_string + ', ' + ticketcc_string += ', ' if strings_to_check.__contains__(ticketcc_this_entry.upper()): - SHOW_SUBSCRIBE = False + show_subscribe = False # check whether current user is a submitter or assigned to ticket assignedto_username = str(ticket.assigned_to).upper() @@ -337,24 +347,30 @@ def return_ticketccstring_and_show_subscribe(user, ticket): strings_to_check.append(assignedto_username) strings_to_check.append(submitter_email) if strings_to_check.__contains__(username) or strings_to_check.__contains__(useremail): - SHOW_SUBSCRIBE = False + show_subscribe = False - return ticketcc_string, SHOW_SUBSCRIBE + return ticketcc_string, show_subscribe def subscribe_staff_member_to_ticket(ticket, user): - ''' used in view_ticket() and update_ticket() ''' - ticketcc = TicketCC() - ticketcc.ticket = ticket - ticketcc.user = user - ticketcc.can_view = True - ticketcc.can_update = True + """used in view_ticket() and update_ticket()""" + ticketcc = TicketCC( + ticket=ticket, + user=user, + can_view=True, + can_update=True, + ) ticketcc.save() def update_ticket(request, ticket_id, public=False): - if not (public or (request.user.is_authenticated() and request.user.is_active and (request.user.is_staff or helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE))): - return HttpResponseRedirect('%s?next=%s' % (reverse('helpdesk:login'), request.path)) + if not (public or ( + request.user.is_authenticated() and + request.user.is_active and ( + request.user.is_staff or + helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE))): + return HttpResponseRedirect('%s?next=%s' % + (reverse('helpdesk:login'), request.path)) ticket = get_object_or_404(Ticket, id=ticket_id) @@ -384,7 +400,8 @@ def update_ticket(request, ticket_id, public=False): title == ticket.title, priority == int(ticket.priority), due_date == ticket.due_date, - (owner == -1) or (not owner and not ticket.assigned_to) or (owner and User.objects.get(id=owner) == ticket.assigned_to), + (owner == -1) or (not owner and not ticket.assigned_to) or + (owner and User.objects.get(id=owner) == ticket.assigned_to), ]) if no_changes: return return_to_ticket(request.user, helpdesk_settings, ticket) @@ -398,7 +415,8 @@ def update_ticket(request, ticket_id, public=False): # then the following line will give us a crash, since django expects {% if %} # to be closed with an {% endif %} tag. - # 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 @@ -428,7 +446,7 @@ def update_ticket(request, ticket_id, public=False): new_user = User.objects.get(id=owner) f.title = _('Assigned to %(username)s') % { 'username': new_user.get_username(), - } + } ticket.assigned_to = new_user reassigned = True # user changed owner to 'unassign' @@ -455,7 +473,7 @@ def update_ticket(request, ticket_id, public=False): files = [] if request.FILES: - import mimetypes, os + import mimetypes for file in request.FILES.getlist('attachment'): filename = file.name.encode('ascii', 'ignore') a = Attachment( @@ -463,7 +481,7 @@ def update_ticket(request, ticket_id, public=False): filename=filename, mime_type=mimetypes.guess_type(filename)[0] or 'application/octet-stream', size=file.size, - ) + ) a.file.save(filename, file, save=False) a.save() @@ -472,14 +490,13 @@ def update_ticket(request, ticket_id, public=False): # settings.MAX_EMAIL_ATTACHMENT_SIZE) are sent via email. files.append([a.filename, a.file]) - if title != ticket.title: c = TicketChange( followup=f, field=_('Title'), old_value=ticket.title, new_value=title, - ) + ) c.save() ticket.title = title @@ -489,7 +506,7 @@ def update_ticket(request, ticket_id, public=False): field=_('Priority'), old_value=ticket.priority, new_value=priority, - ) + ) c.save() ticket.priority = priority @@ -499,11 +516,11 @@ def update_ticket(request, ticket_id, public=False): field=_('Due on'), old_value=ticket.due_date, new_value=due_date, - ) + ) c.save() ticket.due_date = due_date - if new_status in [ Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS ]: + if new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS): if new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None: ticket.resolution = comment @@ -515,11 +532,11 @@ def update_ticket(request, ticket_id, public=False): context.update( resolution=ticket.resolution, comment=f.comment, - ) - - if public and (f.comment or (f.new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS))): - + ) + if public and (f.comment or ( + f.new_status in (Ticket.RESOLVED_STATUS, + Ticket.CLOSED_STATUS))): if f.new_status == Ticket.RESOLVED_STATUS: template = 'resolved_' elif f.new_status == Ticket.CLOSED_STATUS: @@ -537,7 +554,7 @@ def update_ticket(request, ticket_id, public=False): sender=ticket.queue.from_address, fail_silently=True, files=files, - ) + ) messages_sent_to.append(ticket.submitter_email) template_suffix = 'cc' @@ -551,10 +568,13 @@ def update_ticket(request, ticket_id, public=False): sender=ticket.queue.from_address, fail_silently=True, files=files, - ) + ) messages_sent_to.append(cc.email_address) - if ticket.assigned_to and request.user != ticket.assigned_to and ticket.assigned_to.email and ticket.assigned_to.email not in messages_sent_to: + if ticket.assigned_to and \ + request.user != ticket.assigned_to and \ + ticket.assigned_to.email and \ + ticket.assigned_to.email not in messages_sent_to: # We only send e-mails to staff members if the ticket is updated by # another user. The actual template varies, depending on what has been # changed. @@ -567,7 +587,13 @@ def update_ticket(request, ticket_id, public=False): else: template_staff = 'updated_owner' - if (not reassigned or ( reassigned and ticket.assigned_to.usersettings.settings.get('email_on_ticket_assign', False))) or (not reassigned and ticket.assigned_to.usersettings.settings.get('email_on_ticket_change', False)): + if (not reassigned or + (reassigned and + ticket.assigned_to.usersettings.settings.get( + 'email_on_ticket_assign', False))) or \ + (not reassigned and + ticket.assigned_to.usersettings.settings.get( + 'email_on_ticket_change', False)): send_templated_mail( template_staff, context, @@ -575,7 +601,7 @@ def update_ticket(request, ticket_id, public=False): sender=ticket.queue.from_address, fail_silently=True, files=files, - ) + ) messages_sent_to.append(ticket.assigned_to.email) if ticket.queue.updated_ticket_cc and ticket.queue.updated_ticket_cc not in messages_sent_to: @@ -595,7 +621,7 @@ def update_ticket(request, ticket_id, public=False): sender=ticket.queue.from_address, fail_silently=True, files=files, - ) + ) ticket.save() @@ -609,7 +635,7 @@ def update_ticket(request, ticket_id, public=False): def return_to_ticket(user, helpdesk_settings, ticket): - ''' Helpder function for update_ticket ''' + """Helper function for update_ticket""" if user.is_staff or helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE: return HttpResponseRedirect(ticket.get_absolute_url()) @@ -638,29 +664,47 @@ def mass_update(request): if action == 'assign' and t.assigned_to != user: t.assigned_to = user t.save() - f = FollowUp(ticket=t, date=timezone.now(), title=_('Assigned to %(username)s in bulk update' % {'username': user.get_username()}), public=True, user=request.user) + f = FollowUp(ticket=t, + date=timezone.now(), + title=_('Assigned to %(username)s in bulk update' % { + 'username': user.get_username() + }), + public=True, + user=request.user) f.save() elif action == 'unassign' and t.assigned_to is not None: t.assigned_to = None t.save() - f = FollowUp(ticket=t, date=timezone.now(), title=_('Unassigned in bulk update'), public=True, user=request.user) + f = FollowUp(ticket=t, + date=timezone.now(), + title=_('Unassigned in bulk update'), + public=True, + user=request.user) f.save() elif action == 'close' and t.status != Ticket.CLOSED_STATUS: t.status = Ticket.CLOSED_STATUS t.save() - f = FollowUp(ticket=t, date=timezone.now(), title=_('Closed in bulk update'), public=False, user=request.user, new_status=Ticket.CLOSED_STATUS) + f = FollowUp(ticket=t, + date=timezone.now(), + title=_('Closed in bulk update'), + public=False, + user=request.user, + new_status=Ticket.CLOSED_STATUS) f.save() elif action == 'close_public' and t.status != Ticket.CLOSED_STATUS: t.status = Ticket.CLOSED_STATUS t.save() - f = FollowUp(ticket=t, date=timezone.now(), title=_('Closed in bulk update'), public=True, user=request.user, new_status=Ticket.CLOSED_STATUS) + f = FollowUp(ticket=t, + date=timezone.now(), + title=_('Closed in bulk update'), + public=True, + user=request.user, + new_status=Ticket.CLOSED_STATUS) f.save() # Send email to Submitter, Owner, Queue CC context = safe_template_context(t) - context.update( - resolution = t.resolution, - queue = t.queue, - ) + context.update(resolution=t.resolution, + queue=t.queue) messages_sent_to = [] @@ -671,7 +715,7 @@ def mass_update(request): recipients=t.submitter_email, sender=t.queue.from_address, fail_silently=True, - ) + ) messages_sent_to.append(t.submitter_email) for cc in t.ticketcc_set.all(): @@ -682,27 +726,31 @@ def mass_update(request): recipients=cc.email_address, sender=t.queue.from_address, fail_silently=True, - ) + ) messages_sent_to.append(cc.email_address) - if t.assigned_to and request.user != t.assigned_to and t.assigned_to.email and t.assigned_to.email not in messages_sent_to: + if t.assigned_to and \ + request.user != t.assigned_to and \ + t.assigned_to.email and \ + t.assigned_to.email not in messages_sent_to: send_templated_mail( 'closed_owner', context, recipients=t.assigned_to.email, sender=t.queue.from_address, fail_silently=True, - ) + ) messages_sent_to.append(t.assigned_to.email) - if t.queue.updated_ticket_cc and t.queue.updated_ticket_cc not in messages_sent_to: + if t.queue.updated_ticket_cc and \ + t.queue.updated_ticket_cc not in messages_sent_to: send_templated_mail( 'closed_cc', context, recipients=t.queue.updated_ticket_cc, sender=t.queue.from_address, fail_silently=True, - ) + ) elif action == 'delete': t.delete() @@ -710,6 +758,7 @@ def mass_update(request): return HttpResponseRedirect(reverse('helpdesk:list')) mass_update = staff_member_required(mass_update) + def ticket_list(request): context = {} @@ -725,7 +774,7 @@ def ticket_list(request): 'sortreverse': False, 'keyword': None, 'search_string': None, - } + } from_saved_query = False @@ -745,7 +794,7 @@ def ticket_list(request): id = None if id: - filter = {'queue__slug': queue, 'id': id } + filter = {'queue__slug': queue, 'id': id} else: try: query = int(query) @@ -753,7 +802,7 @@ def ticket_list(request): query = None if query: - filter = {'id': int(query) } + filter = {'id': int(query)} if filter: try: @@ -781,13 +830,12 @@ def ticket_list(request): # Query deserialization failed. (E.g. was a pickled query) return HttpResponseRedirect(reverse('helpdesk:list')) - elif not ( 'queue' in request.GET - or 'assigned_to' in request.GET - or 'status' in request.GET - or 'q' in request.GET - or 'sort' in request.GET - or 'sortreverse' in request.GET - ): + elif not ('queue' in request.GET or + 'assigned_to' in request.GET or + 'status' in request.GET or + 'q' in request.GET or + 'sort' in request.GET or + 'sortreverse' in request.GET): # Fall-back if no querying is being done, force the list to only # show open/reopened/resolved (not closed) cases sorted by creation @@ -830,14 +878,14 @@ def ticket_list(request): if date_to: query_params['filtering']['created__lte'] = date_to - ### KEYWORD SEARCHING + # KEYWORD SEARCHING q = request.GET.get('q', None) if q: context = dict(context, query=q) query_params['search_string'] = q - ### SORTING + # SORTING sort = request.GET.get('sort', None) if sort not in ('status', 'assigned_to', 'created', 'title', 'queue', 'priority'): sort = 'created' @@ -858,11 +906,13 @@ def ticket_list(request): } ticket_qs = apply_query(tickets, query_params) - ticket_paginator = paginator.Paginator(ticket_qs, request.user.usersettings.settings.get('tickets_per_page') or 20) + ticket_paginator = paginator.Paginator( + ticket_qs, + request.user.usersettings.settings.get('tickets_per_page') or 20) try: page = int(request.GET.get('page', '1')) except ValueError: - page = 1 + page = 1 try: tickets = ticket_paginator.page(page) @@ -871,8 +921,13 @@ def ticket_list(request): search_message = '' if 'query' in context and settings.DATABASES['default']['ENGINE'].endswith('sqlite'): - search_message = _('

Note: Your keyword search is case sensitive because of your database. This means the search will not be accurate. By switching to a different database system you will gain better searching! For more information, read the Django Documentation on string matching in SQLite.') - + search_message = _( + '

Note: Your keyword search is case sensitive ' + 'because of your database. This means the search will not ' + 'be accurate. By switching to a different database system you will gain ' + 'better searching! For more information, read the ' + '' + 'Django Documentation on string matching in SQLite.') import json from helpdesk.lib import b64encode @@ -883,22 +938,20 @@ def ticket_list(request): querydict = request.GET.copy() querydict.pop('page', 1) - - return render(request, 'helpdesk/ticket_list.html', - dict( - context, - query_string=querydict.urlencode(), - tickets=tickets, - user_choices=User.objects.filter(is_active=True,is_staff=True), - queue_choices=user_queues, - status_choices=Ticket.STATUS_CHOICES, - urlsafe_query=urlsafe_query, - user_saved_queries=user_saved_queries, - query_params=query_params, - from_saved_query=from_saved_query, - saved_query=saved_query, - search_message=search_message, - )) + return render(request, 'helpdesk/ticket_list.html', dict( + context, + query_string=querydict.urlencode(), + tickets=tickets, + user_choices=User.objects.filter(is_active=True, is_staff=True), + queue_choices=user_queues, + status_choices=Ticket.STATUS_CHOICES, + urlsafe_query=urlsafe_query, + user_saved_queries=user_saved_queries, + query_params=query_params, + from_saved_query=from_saved_query, + saved_query=saved_query, + search_message=search_message, + )) ticket_list = staff_member_required(ticket_list) @@ -915,12 +968,10 @@ def edit_ticket(request, ticket_id): else: form = EditTicketForm(instance=ticket) - return render(request, template_name='helpdesk/edit_ticket.html', - context = { - 'form': form, - }) + return render(request, 'helpdesk/edit_ticket.html', {'form': form}) edit_ticket = staff_member_required(edit_ticket) + def create_ticket(request): if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS: assignable_users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) @@ -929,8 +980,10 @@ def create_ticket(request): if request.method == 'POST': form = TicketForm(request.POST, request.FILES) - form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.all()] - form.fields['assigned_to'].choices = [('', '--------')] + [[u.id, u.get_username()] for u in assignable_users] + form.fields['queue'].choices = [('', '--------')] + [ + (q.id, q.title) for q in Queue.objects.all()] + form.fields['assigned_to'].choices = [('', '--------')] + [ + (u.id, u.get_username()) for u in assignable_users] if form.is_valid(): ticket = form.save(user=request.user) if _has_access_to_queue(request.user, ticket.queue): @@ -945,13 +998,14 @@ def create_ticket(request): initial_data['queue'] = request.GET['queue'] form = TicketForm(initial=initial_data) - form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.all()] - form.fields['assigned_to'].choices = [('', '--------')] + [[u.id, u.get_username()] for u in assignable_users] + form.fields['queue'].choices = [('', '--------')] + [ + (q.id, q.title) for q in Queue.objects.all()] + form.fields['assigned_to'].choices = [('', '--------')] + [ + (u.id, u.get_username()) for u in assignable_users] if helpdesk_settings.HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO: form.fields['assigned_to'].widget = forms.HiddenInput() - return render(request, template_name='helpdesk/create_ticket.html', - context = {'form': form}) + return render(request, 'helpdesk/create_ticket.html', {'form': form}) create_ticket = staff_member_required(create_ticket) @@ -960,7 +1014,7 @@ def raw_details(request, type): # in the future it needs to be expanded to include other items. All it # does is return a plain-text representation of an object. - if not type in ('preset',): + if type not in ('preset',): raise Http404 if type == 'preset' and request.GET.get('id', False): @@ -987,11 +1041,11 @@ def hold_ticket(request, ticket_id, unhold=False): title = _('Ticket placed on hold') f = FollowUp( - ticket = ticket, - user = request.user, - title = title, - date = timezone.now(), - public = True, + ticket=ticket, + user=request.user, + title=title, + date=timezone.now(), + public=True, ) f.save() @@ -1007,26 +1061,24 @@ unhold_ticket = staff_member_required(unhold_ticket) def rss_list(request): - return render(request, template_name='helpdesk/rss_list.html', - context = { - 'queues': Queue.objects.all(), - }) + return render(request, 'helpdesk/rss_list.html', {'queues': Queue.objects.all()}) rss_list = staff_member_required(rss_list) def report_index(request): number_tickets = Ticket.objects.all().count() saved_query = request.GET.get('saved_query', None) - return render(request, template_name='helpdesk/report_index.html', - context = { - 'number_tickets': number_tickets, - 'saved_query': saved_query, - }) + return render(request, 'helpdesk/report_index.html', { + 'number_tickets': number_tickets, + 'saved_query': saved_query, + }) report_index = staff_member_required(report_index) def run_report(request, report): - if Ticket.objects.all().count() == 0 or report not in ('queuemonth', 'usermonth', 'queuestatus', 'queuepriority', 'userstatus', 'userpriority', 'userqueue', 'daysuntilticketclosedbymonth'): + if Ticket.objects.all().count() == 0 or report not in ( + 'queuemonth', 'usermonth', 'queuestatus', 'queuepriority', 'userstatus', + 'userpriority', 'userqueue', 'daysuntilticketclosedbymonth'): return HttpResponseRedirect(reverse("helpdesk_report_index")) report_queryset = Ticket.objects.all().select_related().filter( @@ -1059,7 +1111,8 @@ def run_report(request, report): # a second table for more complex queries summarytable2 = defaultdict(int) - month_name = lambda m: MONTHS_3[m].title() + def month_name(m): + MONTHS_3[m].title() first_ticket = Ticket.objects.all().order_by('created')[0] first_month = first_ticket.created.month @@ -1191,22 +1244,21 @@ def run_report(request, report): data.append(summarytable[item, hdr]) table.append([item] + data) - return render(request, 'helpdesk/report_output.html', - { - 'title': title, - 'charttype': charttype, - 'data': table, - 'headings': column_headings, - 'from_saved_query': from_saved_query, - 'saved_query': saved_query, - }) + return render(request, 'helpdesk/report_output.html', { + 'title': title, + 'charttype': charttype, + 'data': table, + 'headings': column_headings, + 'from_saved_query': from_saved_query, + 'saved_query': saved_query, + }) run_report = staff_member_required(run_report) def save_query(request): title = request.POST.get('title', None) shared = request.POST.get('shared', False) - if shared == 'on': # django only translates '1', 'true', 't' into True + if shared == 'on': # django only translates '1', 'true', 't' into True shared = True query_encoded = request.POST.get('query_encoded', None) @@ -1227,10 +1279,7 @@ def delete_saved_query(request, id): query.delete() return HttpResponseRedirect(reverse('helpdesk:list')) else: - return render(request, template_name='helpdesk/confirm_delete_saved_query.html', - context = { - 'query': query, - }) + return render(request, 'helpdesk/confirm_delete_saved_query.html', {'query': query}) delete_saved_query = staff_member_required(delete_saved_query) @@ -1244,18 +1293,14 @@ def user_settings(request): else: form = UserSettingsForm(s.settings) - return render(request, template_name='helpdesk/user_settings.html', - context = { - 'form': form, - }) + return render(request, 'helpdesk/user_settings.html', {'form': form}) user_settings = staff_member_required(user_settings) def email_ignore(request): - return render(request, template_name='helpdesk/email_ignore_list.html', - context = { - 'ignore_list': IgnoreEmail.objects.all(), - }) + return render(request, 'helpdesk/email_ignore_list.html', { + 'ignore_list': IgnoreEmail.objects.all(), + }) email_ignore = superuser_required(email_ignore) @@ -1263,15 +1308,12 @@ def email_ignore_add(request): if request.method == 'POST': form = EmailIgnoreForm(request.POST) if form.is_valid(): - ignore = form.save() + form.save() return HttpResponseRedirect(reverse('helpdesk:email_ignore')) else: form = EmailIgnoreForm(request.GET) - return render(request, template_name='helpdesk/email_ignore_add.html', - context = { - 'form': form, - }) + return render(request, 'helpdesk/email_ignore_add.html', {'form': form}) email_ignore_add = superuser_required(email_ignore_add) @@ -1281,25 +1323,23 @@ def email_ignore_del(request, id): ignore.delete() return HttpResponseRedirect(reverse('helpdesk:email_ignore')) else: - return render(request, template_name='helpdesk/email_ignore_del.html', - context = { - 'ignore': ignore, - }) + return render(request, 'helpdesk/email_ignore_del.html', {'ignore': ignore}) email_ignore_del = superuser_required(email_ignore_del) + def ticket_cc(request, ticket_id): ticket = get_object_or_404(Ticket, id=ticket_id) if not _has_access_to_queue(request.user, ticket.queue): raise PermissionDenied() copies_to = ticket.ticketcc_set.all() - return render(request, template_name='helpdesk/ticket_cc_list.html', - context = { - 'copies_to': copies_to, - 'ticket': ticket, - }) + return render(request, 'helpdesk/ticket_cc_list.html', { + 'copies_to': copies_to, + 'ticket': ticket, + }) ticket_cc = staff_member_required(ticket_cc) + def ticket_cc_add(request, ticket_id): ticket = get_object_or_404(Ticket, id=ticket_id) if not _has_access_to_queue(request.user, ticket.queue): @@ -1311,28 +1351,28 @@ def ticket_cc_add(request, ticket_id): ticketcc = form.save(commit=False) ticketcc.ticket = ticket ticketcc.save() - return HttpResponseRedirect(reverse('helpdesk:ticket_cc', kwargs={'ticket_id': ticket.id})) + return HttpResponseRedirect(reverse('helpdesk:ticket_cc', + kwargs={'ticket_id': ticket.id})) else: form = TicketCCForm() - return render(request, template_name='helpdesk/ticket_cc_add.html', - context = { - 'ticket': ticket, - 'form': form, - }) + return render(request, 'helpdesk/ticket_cc_add.html', { + 'ticket': ticket, + 'form': form, + }) ticket_cc_add = staff_member_required(ticket_cc_add) + def ticket_cc_del(request, ticket_id, cc_id): cc = get_object_or_404(TicketCC, ticket__id=ticket_id, id=cc_id) if request.method == 'POST': cc.delete() - return HttpResponseRedirect(reverse('helpdesk:ticket_cc', kwargs={'ticket_id': cc.ticket.id})) - return render(request, template_name='helpdesk/ticket_cc_del.html', - context = { - 'cc': cc, - }) + return HttpResponseRedirect(reverse('helpdesk:ticket_cc', + kwargs={'ticket_id': cc.ticket.id})) + return render(request, 'helpdesk/ticket_cc_del.html', {'cc': cc}) ticket_cc_del = staff_member_required(ticket_cc_del) + def ticket_dependency_add(request, ticket_id): ticket = get_object_or_404(Ticket, id=ticket_id) if not _has_access_to_queue(request.user, ticket.queue): @@ -1347,24 +1387,22 @@ def ticket_dependency_add(request, ticket_id): return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket.id])) else: form = TicketDependencyForm() - return render(request, template_name='helpdesk/ticket_dependency_add.html', - context = { - 'ticket': ticket, - 'form': form, - }) + return render(request, 'helpdesk/ticket_dependency_add.html', { + 'ticket': ticket, + 'form': form, + }) ticket_dependency_add = staff_member_required(ticket_dependency_add) + def ticket_dependency_del(request, ticket_id, dependency_id): dependency = get_object_or_404(TicketDependency, ticket__id=ticket_id, id=dependency_id) if request.method == 'POST': dependency.delete() return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket_id])) - return render(request, template_name='helpdesk/ticket_dependency_del.html', - context = { - 'dependency': dependency, - }) + return render(request, 'helpdesk/ticket_dependency_del.html', {'dependency': dependency}) ticket_dependency_del = staff_member_required(ticket_dependency_del) + def attachment_del(request, ticket_id, attachment_id): ticket = get_object_or_404(Ticket, id=ticket_id) if not _has_access_to_queue(request.user, ticket.queue): @@ -1374,6 +1412,7 @@ def attachment_del(request, ticket_id, attachment_id): return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket_id])) attachment_del = staff_member_required(attachment_del) + def calc_average_nbr_days_until_ticket_resolved(Tickets): nbr_closed_tickets = len(Tickets) days_per_ticket = 0 @@ -1392,9 +1431,10 @@ def calc_average_nbr_days_until_ticket_resolved(Tickets): return mean_per_ticket + def calc_basic_ticket_stats(Tickets): # all not closed tickets (open, reopened, resolved,) - independent of user - all_open_tickets = Tickets.exclude(status = Ticket.CLOSED_STATUS) + all_open_tickets = Tickets.exclude(status=Ticket.CLOSED_STATUS) today = datetime.today() date_30 = date_rel_to_today(today, 30) @@ -1403,57 +1443,66 @@ def calc_basic_ticket_stats(Tickets): date_60_str = date_60.strftime('%Y-%m-%d') # > 0 & <= 30 - ota_le_30 = all_open_tickets.filter(created__gte = date_30_str) + ota_le_30 = all_open_tickets.filter(created__gte=date_30_str) N_ota_le_30 = len(ota_le_30) # >= 30 & <= 60 - ota_le_60_ge_30 = all_open_tickets.filter(created__gte = date_60_str, created__lte = date_30_str) + ota_le_60_ge_30 = all_open_tickets.filter(created__gte=date_60_str, created__lte=date_30_str) N_ota_le_60_ge_30 = len(ota_le_60_ge_30) # >= 60 - ota_ge_60 = all_open_tickets.filter(created__lte = date_60_str) + ota_ge_60 = all_open_tickets.filter(created__lte=date_60_str) N_ota_ge_60 = len(ota_ge_60) # (O)pen (T)icket (S)tats ots = list() # label, number entries, color, sort_string - ots.append(['< 30 days', N_ota_le_30, get_color_for_nbr_days(N_ota_le_30), sort_string(date_30_str, ''), ]) - ots.append(['30 - 60 days', N_ota_le_60_ge_30, get_color_for_nbr_days(N_ota_le_60_ge_30), sort_string(date_60_str, date_30_str), ]) - ots.append(['> 60 days', N_ota_ge_60, get_color_for_nbr_days(N_ota_ge_60), sort_string('', date_60_str), ]) + ots.append(['< 30 days', N_ota_le_30, get_color_for_nbr_days(N_ota_le_30), + sort_string(date_30_str, ''), ]) + ots.append(['30 - 60 days', N_ota_le_60_ge_30, get_color_for_nbr_days(N_ota_le_60_ge_30), + sort_string(date_60_str, date_30_str), ]) + ots.append(['> 60 days', N_ota_ge_60, get_color_for_nbr_days(N_ota_ge_60), + sort_string('', date_60_str), ]) # all closed tickets - independent of user. - all_closed_tickets = Tickets.filter(status = Ticket.CLOSED_STATUS) - average_nbr_days_until_ticket_closed = calc_average_nbr_days_until_ticket_resolved(all_closed_tickets) + all_closed_tickets = Tickets.filter(status=Ticket.CLOSED_STATUS) + average_nbr_days_until_ticket_closed = \ + calc_average_nbr_days_until_ticket_resolved(all_closed_tickets) # all closed tickets that were opened in the last 60 days. - all_closed_last_60_days = all_closed_tickets.filter(created__gte = date_60_str) - average_nbr_days_until_ticket_closed_last_60_days = calc_average_nbr_days_until_ticket_resolved(all_closed_last_60_days) + all_closed_last_60_days = all_closed_tickets.filter(created__gte=date_60_str) + average_nbr_days_until_ticket_closed_last_60_days = \ + calc_average_nbr_days_until_ticket_resolved(all_closed_last_60_days) # put together basic stats - basic_ticket_stats = { 'average_nbr_days_until_ticket_closed': average_nbr_days_until_ticket_closed, - 'average_nbr_days_until_ticket_closed_last_60_days': average_nbr_days_until_ticket_closed_last_60_days, - 'open_ticket_stats': ots, } + basic_ticket_stats = { + 'average_nbr_days_until_ticket_closed': average_nbr_days_until_ticket_closed, + 'average_nbr_days_until_ticket_closed_last_60_days': + average_nbr_days_until_ticket_closed_last_60_days, + 'open_ticket_stats': ots, + } return basic_ticket_stats + def get_color_for_nbr_days(nbr_days): - ''' ''' if nbr_days < 5: color_string = 'green' - elif nbr_days >= 5 and nbr_days < 10: + elif nbr_days < 10: color_string = 'orange' - else: # more than 10 days + else: # more than 10 days color_string = 'red' return color_string + def days_since_created(today, ticket): return (today - ticket.created).days + def date_rel_to_today(today, offset): - return today - timedelta(days = offset) + return today - timedelta(days=offset) + def sort_string(begin, end): - return 'sort=created&date_from=%s&date_to=%s&status=%s&status=%s&status=%s' %(begin, end, Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS, Ticket.RESOLVED_STATUS) - - - + return 'sort=created&date_from=%s&date_to=%s&status=%s&status=%s&status=%s' % ( + begin, end, Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS, Ticket.RESOLVED_STATUS) diff --git a/quicktest.py b/quicktest.py index c3170bc5..479dbadb 100644 --- a/quicktest.py +++ b/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) -