From 24d88be8d92bb108a73c7a6c710afd1ef1afb987 Mon Sep 17 00:00:00 2001 From: Alex Barcelo Date: Fri, 21 Oct 2016 17:14:12 +0200 Subject: [PATCH 1/2] Initial general clean-up of stuff --- helpdesk/admin.py | 20 +- helpdesk/akismet.py | 33 +- helpdesk/apps.py | 2 +- helpdesk/forms.py | 138 ++--- helpdesk/lib.py | 71 +-- .../commands/create_escalation_exclusions.py | 12 +- .../commands/create_usersettings.py | 20 +- .../management/commands/escalate_tickets.py | 37 +- helpdesk/management/commands/get_email.py | 97 ++-- .../migrations/0003_initial_data_import.py | 2 +- helpdesk/models.py | 176 +++--- helpdesk/settings.py | 87 ++- helpdesk/templatetags/in_list.py | 3 +- .../templatetags/load_helpdesk_settings.py | 8 +- helpdesk/templatetags/ticket_to_link.py | 19 +- helpdesk/templatetags/user_admin_url.py | 1 + helpdesk/tests/helpers.py | 9 +- helpdesk/tests/test_public_actions.py | 25 +- helpdesk/tests/test_savequery.py | 22 +- helpdesk/tests/test_ticket_lookup.py | 16 +- helpdesk/tests/test_ticket_submission.py | 66 ++- helpdesk/urls.py | 13 +- helpdesk/views/api.py | 36 +- helpdesk/views/feeds.py | 13 +- helpdesk/views/kb.py | 34 +- helpdesk/views/public.py | 61 +-- helpdesk/views/staff.py | 516 ++++++++++-------- 27 files changed, 827 insertions(+), 710 deletions(-) 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 ed0993c0..44d3be8f 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,23 +15,22 @@ 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_lazy 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.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 @@ -52,7 +53,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,6 +74,7 @@ class CustomFieldMixin(object): self.fields['custom_%s' % field.name] = fieldclass(**instanceargs) + class EditTicketForm(CustomFieldMixin, forms.ModelForm): class Meta: model = Ticket @@ -99,7 +101,6 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm): self.customfield_to_field(field, instanceargs) - def save(self, *args, **kwargs): for field, value in self.cleaned_data.items(): @@ -108,7 +109,7 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm): 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() @@ -117,14 +118,16 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm): 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'), @@ -158,7 +161,7 @@ class TicketForm(CustomFieldMixin, forms.Form): 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( @@ -166,8 +169,7 @@ class TicketForm(CustomFieldMixin, forms.Form): 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( @@ -178,9 +180,9 @@ class TicketForm(CustomFieldMixin, forms.Form): 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( @@ -203,7 +205,6 @@ class TicketForm(CustomFieldMixin, forms.Form): self.customfield_to_field(field, instanceargs) - def save(self, user): """ Writes and returns a Ticket() object @@ -211,15 +212,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: @@ -234,16 +235,16 @@ class TicketForm(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'), - 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') % { @@ -290,7 +291,11 @@ class TicketForm(CustomFieldMixin, forms.Form): ) 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, @@ -312,7 +317,9 @@ class TicketForm(CustomFieldMixin, forms.Form): ) 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, @@ -350,7 +357,7 @@ class PublicTicketForm(CustomFieldMixin, forms.Form): 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( @@ -396,14 +403,14 @@ 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: @@ -416,16 +423,16 @@ 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() @@ -463,7 +470,10 @@ class PublicTicketForm(CustomFieldMixin, forms.Form): ) 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, @@ -485,7 +495,9 @@ class PublicTicketForm(CustomFieldMixin, forms.Form): ) 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, @@ -537,12 +549,18 @@ class UserSettingsForm(forms.Form): 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: @@ -550,9 +568,7 @@ class TicketCCForm(forms.ModelForm): else: users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD) self.fields['user'].queryset = users - class Meta: - model = TicketCC - exclude = ('ticket',) + class TicketDependencyForm(forms.ModelForm): class Meta: diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 95dddbd0..84a60b2f 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 @@ -105,21 +113,22 @@ def send_templated_mail(template_name, email_context, recipients, sender=None, b 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: @@ -230,19 +237,19 @@ def safe_template_context(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..6c011f1b 100644 --- a/helpdesk/management/commands/create_escalation_exclusions.py +++ b/helpdesk/management/commands/create_escalation_exclusions.py @@ -16,7 +16,6 @@ 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 @@ -47,7 +46,8 @@ class Command(BaseCommand): 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 +55,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 +114,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 +123,7 @@ if __name__ == '__main__': sys.exit(2) days = None - occurrences = None + occurrences = 1 verbose = False queue_slugs = None queues = [] @@ -139,9 +136,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_usersettings.py b/helpdesk/management/commands/create_usersettings.py index eafa6278..965d347f 100644 --- a/helpdesk/management/commands/create_usersettings.py +++ b/helpdesk/management/commands/create_usersettings.py @@ -10,17 +10,16 @@ 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..a3df890a 100644 --- a/helpdesk/management/commands/escalate_tickets.py +++ b/helpdesk/management/commands/escalate_tickets.py @@ -56,7 +56,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 +82,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 @@ -143,8 +142,8 @@ def escalate_tickets(queues, verbose): ) 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 +151,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..287cff0f 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -40,6 +40,14 @@ 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) @@ -52,7 +60,8 @@ class Command(BaseCommand): 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) @@ -70,7 +79,6 @@ def process_email(quiet=False): 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 +98,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 +150,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') @@ -160,22 +185,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)) @@ -210,9 +239,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: @@ -259,7 +289,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,18 +300,18 @@ 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: @@ -308,7 +338,6 @@ def ticket_from_message(message, queue, quiet): if not quiet: print(" - %s" % filename) - context = safe_template_context(t) if new: @@ -343,10 +372,10 @@ def ticket_from_message(message, queue, quiet): 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( 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 a3a9c7e4..4ee9ab08 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: @@ -47,7 +43,7 @@ class Queue(models.Model): 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( @@ -55,8 +51,8 @@ class Queue(models.Model): 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( @@ -64,15 +60,15 @@ class Queue(models.Model): 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( @@ -80,7 +76,7 @@ class Queue(models.Model): blank=True, default=False, help_text=_('Do you want to poll the e-mail box below for new ' - 'tickets?'), + 'tickets?'), ) escalate_days = models.IntegerField( @@ -88,7 +84,7 @@ class Queue(models.Model): 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( @@ -97,8 +93,8 @@ 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( @@ -107,9 +103,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 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( @@ -119,7 +115,7 @@ 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( @@ -128,7 +124,7 @@ 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( @@ -136,8 +132,8 @@ class Queue(models.Model): 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( @@ -145,7 +141,7 @@ class Queue(models.Model): 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( @@ -170,9 +166,9 @@ 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( @@ -375,7 +371,7 @@ class Ticket(models.Model): 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( @@ -396,8 +392,7 @@ class Ticket(models.Model): _('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( @@ -433,7 +428,7 @@ class Ticket(models.Model): 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): @@ -453,7 +448,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): @@ -486,7 +481,8 @@ class Ticket(models.Model): held_msg = '' 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) @@ -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('-') @@ -621,7 +618,7 @@ class FollowUp(models.Model): 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( @@ -785,33 +782,32 @@ 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.'), + '{{ 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') - def __str__(self): return '%s' % self.name @@ -831,9 +827,8 @@ 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( @@ -873,29 +868,28 @@ class EmailTemplate(models.Model): _('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( @@ -944,7 +938,7 @@ class KBCategory(models.Model): verbose_name_plural = _('Knowledge base categories') def get_absolute_url(self): - return ('helpdesk_kb_category', (), {'slug': self.slug}) + return 'helpdesk_kb_category', (), {'slug': self.slug} get_absolute_url = models.permalink(get_absolute_url) @@ -986,8 +980,7 @@ class KBItem(models.Model): 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, ) @@ -1012,7 +1005,7 @@ class KBItem(models.Model): 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) @@ -1076,7 +1069,8 @@ 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, ) @@ -1125,15 +1119,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,12 +1129,15 @@ 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( @@ -1167,16 +1156,15 @@ class IgnoreEmail(models.Model): _('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): @@ -1202,18 +1190,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): @@ -1277,6 +1261,7 @@ 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,7 +1275,8 @@ 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, ) @@ -1346,7 +1332,8 @@ class CustomField(models.Model): 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( @@ -1379,14 +1366,15 @@ class CustomField(models.Model): 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') @@ -1423,6 +1411,11 @@ 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'), @@ -1437,8 +1430,3 @@ class TicketDependency(models.Model): 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..835efe81 100644 --- a/helpdesk/templatetags/load_helpdesk_settings.py +++ b/helpdesk/templatetags/load_helpdesk_settings.py @@ -4,17 +4,19 @@ django-helpdesk - A Django powered ticket tracker for small enterprise. 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/ticket_to_link.py b/helpdesk/templatetags/ticket_to_link.py index 2e72729a..4fc65dd9 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 95ccfb70..4133de70 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: @@ -15,13 +15,23 @@ class PublicActionsTestCase(TestCase): """ 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') @@ -38,7 +48,10 @@ class PublicActionsTestCase(TestCase): 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) diff --git a/helpdesk/tests/test_savequery.py b/helpdesk/tests/test_savequery.py index df95b9e7..f9d142b2 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 39999421..6e55ce6f 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 e5f9933b..3776dc01 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) @@ -49,7 +58,7 @@ class TicketBasicsTestCase(TestCase): 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 @@ -63,12 +72,12 @@ class TicketBasicsTestCase(TestCase): 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,23 +86,34 @@ 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 diff --git a/helpdesk/urls.py b/helpdesk/urls.py index be64ab81..8a629e63 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 4efd75b4..c476ca9c 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' @@ -101,7 +99,7 @@ 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( @@ -113,7 +111,6 @@ class UnassignedTickets(Feed): 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] 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 c0f36c80..4ff174c6 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('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,14 +35,15 @@ 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) @@ -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', - { + 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: @@ -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 7faed93e..c14ebf40 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) @@ -92,13 +100,13 @@ def dashboard(request): tickets = Ticket.objects.select_related('queue').filter( assigned_to=request.user, ).exclude( - status__in = [Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS], + status__in=[Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS], ) # closed & resolved tickets, assigned to current user - tickets_closed_resolved = Ticket.objects.select_related('queue').filter( + tickets_closed_resolved = Ticket.objects.select_related('queue').filter( assigned_to=request.user, - status__in = [Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS]) + status__in=[Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS]) user_queues = _get_user_queues(request.user) @@ -117,10 +125,10 @@ def dashboard(request): submitter_email=email_current_user, ).order_by('status') - Tickets = Ticket.objects.filter( + tickets_in_queues = Ticket.objects.filter( queue__in=user_queues, ) - basic_ticket_stats = calc_basic_ticket_stats(Tickets) + 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. @@ -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])) @@ -292,24 +301,26 @@ def view_ticket(request, ticket_id): # 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 +332,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 +348,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('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('login'), request.path)) ticket = get_object_or_404(Ticket, id=ticket_id) @@ -384,7 +401,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 +416,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 @@ -455,7 +474,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( @@ -472,7 +491,6 @@ 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, @@ -517,9 +535,9 @@ def update_ticket(request, ticket_id, public=False): 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: @@ -554,7 +572,10 @@ def update_ticket(request, ticket_id, public=False): ) 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 +588,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, @@ -609,7 +636,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 +665,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 = [] @@ -685,7 +730,10 @@ def mass_update(request): ) 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, @@ -695,7 +743,8 @@ def mass_update(request): ) 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, @@ -710,6 +759,7 @@ def mass_update(request): return HttpResponseRedirect(reverse('helpdesk_list')) mass_update = staff_member_required(mass_update) + def ticket_list(request): context = {} @@ -745,7 +795,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 +803,7 @@ def ticket_list(request): query = None if query: - filter = {'id': int(query) } + filter = {'id': int(query)} if filter: try: @@ -781,13 +831,13 @@ 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 +880,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,7 +908,9 @@ 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: @@ -871,8 +923,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 +940,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 +970,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 +982,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 +1000,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 +1016,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 +1043,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 +1063,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( @@ -1191,15 +1245,14 @@ 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) @@ -1227,10 +1280,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 +1294,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 +1309,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 +1324,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 +1352,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 +1388,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 +1413,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 +1432,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 +1444,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) + 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) From 94d51cb8178c2e959f1d8d4972fdd15d9c0d2d58 Mon Sep 17 00:00:00 2001 From: Alex Barcelo Date: Fri, 21 Oct 2016 17:14:34 +0200 Subject: [PATCH 2/2] Minimal settings for .pylintrc (loading pylint_django plugin) --- .pylintrc | 378 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 378 insertions(+) create mode 100644 .pylintrc 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