diff --git a/demo/demodesk/config/settings.py b/demo/demodesk/config/settings.py index ec301a91..5aaccfbf 100644 --- a/demo/demodesk/config/settings.py +++ b/demo/demodesk/config/settings.py @@ -97,13 +97,13 @@ WSGI_APPLICATION = 'demo.demodesk.config.wsgi.application' # Some common settings are below. HELPDESK_DEFAULT_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, - 'preset_replies': 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, + 'preset_replies': True, + 'tickets_per_page': 25 } # Should the public web portal be enabled? @@ -153,7 +153,7 @@ SITE_ID = 1 # Sessions # https://docs.djangoproject.com/en/1.11/topics/http/sessions -SESSION_COOKIE_AGE = 86400 # = 1 day +SESSION_COOKIE_AGE = 86400 # = 1 day # For better default security, set these cookie flags, but # these are likely to cause problems when testing locally diff --git a/demo/demodesk/manage.py b/demo/demodesk/manage.py index 3427b7bc..0bc07500 100755 --- a/demo/demodesk/manage.py +++ b/demo/demodesk/manage.py @@ -2,6 +2,7 @@ import os import sys + def main(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demodesk.config.settings") try: @@ -21,5 +22,6 @@ def main(): raise execute_from_command_line(sys.argv) + if __name__ == "__main__": main() diff --git a/demo/manage.py b/demo/manage.py index 3427b7bc..0bc07500 100755 --- a/demo/manage.py +++ b/demo/manage.py @@ -2,6 +2,7 @@ import os import sys + def main(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demodesk.config.settings") try: @@ -21,5 +22,6 @@ def main(): raise execute_from_command_line(sys.argv) + if __name__ == "__main__": main() diff --git a/demo/setup.py b/demo/setup.py index 01c009bf..2cab27a8 100644 --- a/demo/setup.py +++ b/demo/setup.py @@ -28,7 +28,7 @@ KEYWORDS = [] PACKAGES = ['demodesk'] REQUIREMENTS = [ 'django-helpdesk' - ] +] ENTRY_POINTS = { 'console_scripts': ['demodesk = demodesk.manage:main'] } diff --git a/docs/conf.py b/docs/conf.py index 3d4ce533..6e384cb3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,14 +11,15 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys +import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) -# -- General configuration ----------------------------------------------------- +# -- General configuration ----------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' @@ -87,7 +88,7 @@ pygments_style = 'sphinx' #modindex_common_prefix = [] -# -- Options for HTML output --------------------------------------------------- +# -- Options for HTML output --------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. @@ -167,7 +168,7 @@ html_static_path = ['_static'] htmlhelp_basename = 'django-helpdeskdoc' -# -- Options for LaTeX output -------------------------------------------------- +# -- Options for LaTeX output -------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' @@ -178,8 +179,8 @@ htmlhelp_basename = 'django-helpdeskdoc' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'django-helpdesk.tex', u'django-helpdesk Documentation', - u'Ross Poulton + django-helpdesk Contributors', 'manual'), + ('index', 'django-helpdesk.tex', u'django-helpdesk Documentation', + u'Ross Poulton + django-helpdesk Contributors', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -206,7 +207,7 @@ latex_documents = [ #latex_domain_indices = True -# -- Options for manual page output -------------------------------------------- +# -- Options for manual page output -------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). diff --git a/helpdesk/admin.py b/helpdesk/admin.py index b074d5ae..6fbd352d 100644 --- a/helpdesk/admin.py +++ b/helpdesk/admin.py @@ -9,6 +9,7 @@ if helpdesk_settings.HELPDESK_KB_ENABLED: from helpdesk.models import KBCategory from helpdesk.models import KBItem + @admin.register(Queue) class QueueAdmin(admin.ModelAdmin): list_display = ('title', 'slug', 'email_address', 'locale', 'time_spent') @@ -74,7 +75,8 @@ class FollowUpAdmin(admin.ModelAdmin): if helpdesk_settings.HELPDESK_KB_ENABLED: @admin.register(KBItem) class KBItemAdmin(admin.ModelAdmin): - list_display = ('category', 'title', 'last_updated', 'team', 'order', 'enabled') + list_display = ('category', 'title', 'last_updated', + 'team', 'order', 'enabled') inlines = [KBIAttachmentInline] readonly_fields = ('voted_by', 'downvoted_by') diff --git a/helpdesk/apps.py b/helpdesk/apps.py index fff4c8f2..a3ed19d2 100644 --- a/helpdesk/apps.py +++ b/helpdesk/apps.py @@ -5,5 +5,6 @@ class HelpdeskConfig(AppConfig): name = 'helpdesk' verbose_name = "Helpdesk" # for Django 3.2 support: - # see: https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.default_auto_field + # see: + # https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.default_auto_field default_auto_field = 'django.db.models.AutoField' diff --git a/helpdesk/email.py b/helpdesk/email.py index 537f9af4..6fe347ac 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -72,7 +72,8 @@ def process_email(quiet=False): # Log messages to specific file only if the queue has it configured if (q.logging_type in logging_types) and q.logging_dir: # if it's enabled and the dir is set - log_file_handler = logging.FileHandler(join(q.logging_dir, q.slug + '_get_email.log')) + log_file_handler = logging.FileHandler( + join(q.logging_dir, q.slug + '_get_email.log')) logger.addHandler(log_file_handler) else: log_file_handler = None @@ -105,7 +106,8 @@ def pop3_sync(q, logger, server): try: server.stls() except Exception: - logger.warning("POP3 StartTLS failed or unsupported. Connection will be unencrypted.") + logger.warning( + "POP3 StartTLS failed or unsupported. Connection will be unencrypted.") server.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER) server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD) @@ -127,16 +129,21 @@ def pop3_sync(q, logger, server): raw_content = server.retr(msgNum)[1] if type(raw_content[0]) is bytes: - full_message = "\n".join([elm.decode('utf-8') for elm in raw_content]) + full_message = "\n".join([elm.decode('utf-8') + for elm in raw_content]) else: - full_message = encoding.force_str("\n".join(raw_content), errors='replace') - ticket = object_from_message(message=full_message, queue=q, logger=logger) + full_message = encoding.force_str( + "\n".join(raw_content), errors='replace') + ticket = object_from_message( + message=full_message, queue=q, logger=logger) if ticket: server.dele(msgNum) - logger.info("Successfully processed message %s, deleted from POP3 server" % msgNum) + logger.info( + "Successfully processed message %s, deleted from POP3 server" % msgNum) else: - logger.warn("Message %s was not successfully processed, and will be left on POP3 server" % msgNum) + logger.warn( + "Message %s was not successfully processed, and will be left on POP3 server" % msgNum) server.quit() @@ -146,7 +153,8 @@ def imap_sync(q, logger, server): try: server.starttls() except Exception: - logger.warning("IMAP4 StartTLS unsupported or failed. Connection will be unencrypted.") + logger.warning( + "IMAP4 StartTLS unsupported or failed. Connection will be unencrypted.") server.login(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER, q.email_box_pass or @@ -177,14 +185,17 @@ def imap_sync(q, logger, server): status, data = server.fetch(num, '(RFC822)') full_message = encoding.force_str(data[0][1], errors='replace') try: - ticket = object_from_message(message=full_message, queue=q, logger=logger) + ticket = object_from_message( + message=full_message, queue=q, logger=logger) except TypeError: ticket = None # hotfix. Need to work out WHY. if ticket: server.store(num, '+FLAGS', '\\Deleted') - logger.info("Successfully processed message %s, deleted from IMAP server" % num) + logger.info( + "Successfully processed message %s, deleted from IMAP server" % num) else: - logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % num) + logger.warn( + "Message %s was not successfully processed, and will be left on IMAP server" % num) except imaplib.IMAP4.error: logger.error( "IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?", @@ -261,7 +272,8 @@ def process_queue(q, logger): elif email_box_type == 'local': mail_dir = q.email_box_local_dir or '/var/lib/mail/helpdesk/' - mail = [join(mail_dir, f) for f in os.listdir(mail_dir) if isfile(join(mail_dir, f))] + mail = [join(mail_dir, f) + for f in os.listdir(mail_dir) if isfile(join(mail_dir, f))] logger.info("Found %d messages in local mailbox directory" % len(mail)) logger.info("Found %d messages in local mailbox directory" % len(mail)) @@ -269,17 +281,22 @@ def process_queue(q, logger): logger.info("Processing message %d" % i) with open(m, 'r') as f: full_message = encoding.force_str(f.read(), errors='replace') - ticket = object_from_message(message=full_message, queue=q, logger=logger) + ticket = object_from_message( + message=full_message, queue=q, logger=logger) if ticket: - logger.info("Successfully processed message %d, ticket/comment created.", i) + logger.info( + "Successfully processed message %d, ticket/comment created.", i) try: - os.unlink(m) # delete message file if ticket was successful + # delete message file if ticket was successful + os.unlink(m) except OSError as e: - logger.error("Unable to delete message %d (%s).", i, str(e)) + logger.error( + "Unable to delete message %d (%s).", i, str(e)) else: logger.info("Successfully deleted message %d.", i) else: - logger.warn("Message %d was not successfully processed, and will be left in local directory", i) + logger.warn( + "Message %d was not successfully processed, and will be left in local directory", i) def decodeUnknown(charset, string): @@ -309,8 +326,10 @@ def is_autoreply(message): So we don't start mail loops """ any_if_this = [ - False if not message.get("Auto-Submitted") else message.get("Auto-Submitted").lower() != "no", - True if message.get("X-Auto-Response-Suppress") in ("DR", "AutoReply", "All") else False, + False if not message.get( + "Auto-Submitted") else message.get("Auto-Submitted").lower() != "no", + True if message.get("X-Auto-Response-Suppress") in ("DR", + "AutoReply", "All") else False, message.get("List-Id"), message.get("List-Unsubscribe"), ] @@ -340,7 +359,8 @@ def create_ticket_cc(ticket, cc_list): pass try: - ticket_cc = subscribe_to_ticket_updates(ticket=ticket, user=user, email=cced_email) + ticket_cc = subscribe_to_ticket_updates( + ticket=ticket, user=user, email=cced_email) new_ticket_ccs.append(ticket_cc) except ValidationError: pass @@ -370,7 +390,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger) if in_reply_to is not None: try: - queryset = FollowUp.objects.filter(message_id=in_reply_to).order_by('-date') + queryset = FollowUp.objects.filter( + message_id=in_reply_to).order_by('-date') if queryset.count() > 0: previous_followup = queryset.first() ticket = previous_followup.ticket @@ -386,7 +407,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger) new = False # Check if the ticket has been merged to another ticket if ticket.merged_to: - logger.info("Ticket has been merged to %s" % ticket.merged_to.ticket) + logger.info("Ticket has been merged to %s" % + ticket.merged_to.ticket) # Use the ticket in which it was merged to for next operations ticket = ticket.merged_to @@ -402,7 +424,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger) priority=payload['priority'], ) ticket.save() - logger.debug("Created new ticket %s-%s" % (ticket.queue.slug, ticket.id)) + logger.debug("Created new ticket %s-%s" % + (ticket.queue.slug, ticket.id)) new = True @@ -413,7 +436,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger) f = FollowUp( ticket=ticket, - title=_('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}), + title=_('E-Mail Received from %(sender_email)s' % + {'sender_email': sender_email}), date=now, public=True, comment=payload.get('full_body', payload['body']) or "", @@ -422,7 +446,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger) if ticket.status == Ticket.REOPENED_STATUS: f.new_status = Ticket.REOPENED_STATUS - f.title = _('Ticket Re-Opened by E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}) + f.title = _('Ticket Re-Opened by E-Mail Received from %(sender_email)s' % + {'sender_email': sender_email}) f.save() logger.debug("Created new FollowUp for Ticket") @@ -445,14 +470,16 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger) if queue.enable_notifications_on_email_events and len(notifications_to_be_sent): - ticket_cc_list = TicketCC.objects.filter(ticket=ticket).all().values_list('email', flat=True) + ticket_cc_list = TicketCC.objects.filter( + ticket=ticket).all().values_list('email', flat=True) for email_address in ticket_cc_list: notifications_to_be_sent.append(email_address) autoreply = is_autoreply(message) if autoreply: - logger.info("Message seems to be auto-reply, not sending any emails back to the sender") + logger.info( + "Message seems to be auto-reply, not sending any emails back to the sender") else: # send mail to appropriate people now depending on what objects # were created and who was CC'd @@ -494,7 +521,8 @@ def object_from_message(message, queue, logger): message = email.message_from_string(message) subject = message.get('subject', _('Comment from e-mail')) - subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject)) + subject = decode_mail_headers( + decodeUnknown(message.get_charset(), subject)) for affix in STRIPPED_SUBJECT_STRINGS: subject = subject.replace(affix, "") subject = subject.strip() @@ -508,13 +536,16 @@ def object_from_message(message, queue, logger): # Note that the replace won't work on just an email with no real name, # but the getaddresses() function seems to be able to handle just unclosed quotes # correctly. Not ideal, but this seems to work for now. - sender_email = email.utils.getaddresses(['\"' + sender.replace('<', '\" <')])[0][1] + sender_email = email.utils.getaddresses( + ['\"' + sender.replace('<', '\" <')])[0][1] cc = message.get_all('cc', None) if cc: # first, fixup the encoding if necessary - cc = [decode_mail_headers(decodeUnknown(message.get_charset(), x)) for x in cc] - # get_all checks if multiple CC headers, but individual emails may be comma separated too + cc = [decode_mail_headers(decodeUnknown( + message.get_charset(), x)) for x in cc] + # get_all checks if multiple CC headers, but individual emails may be + # comma separated too tempcc = [] for hdr in cc: tempcc.extend(hdr.split(',')) @@ -561,14 +592,16 @@ def object_from_message(message, queue, logger): # have to use django_settings here so overwritting it works in tests # the default value is False anyway if ticket is None and getattr(django_settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False): - # first message in thread, we save full body to avoid losing forwards and things like that + # first message in thread, we save full body to avoid + # losing forwards and things like that body_parts = [] for f in EmailReplyParser.read(body).fragments: body_parts.append(f.content) full_body = '\n\n'.join(body_parts) body = EmailReplyParser.parse_reply(body) else: - # second and other reply, save only first part of the message + # second and other reply, save only first part of the + # message body = EmailReplyParser.parse_reply(body) full_body = body # workaround to get unicode text out rather than escaped text @@ -579,13 +612,17 @@ def object_from_message(message, queue, logger): logger.debug("Discovered plain text MIME part") else: try: - email_body = encoding.smart_str(part.get_payload(decode=True)) + email_body = encoding.smart_str( + part.get_payload(decode=True)) except UnicodeDecodeError: - email_body = encoding.smart_str(part.get_payload(decode=False)) + email_body = encoding.smart_str( + part.get_payload(decode=False)) if not body and not full_body: - # no text has been parsed so far - try such deep parsing for some messages - altered_body = email_body.replace("

", "

\n").replace("", "

\n").replace("' ) % email_body files.append( - SimpleUploadedFile(_("email_html_body.html"), payload.encode("utf-8"), 'text/html') + SimpleUploadedFile( + _("email_html_body.html"), payload.encode("utf-8"), 'text/html') ) logger.debug("Discovered HTML MIME part") else: @@ -627,7 +665,8 @@ def object_from_message(message, queue, logger): # except non_b64_err: # logger.debug("Payload was not base64 encoded, using raw bytes") # # payloadToWrite = payload - files.append(SimpleUploadedFile(name, part.get_payload(decode=True), mimetypes.guess_type(name)[0])) + files.append(SimpleUploadedFile(name, part.get_payload( + decode=True), mimetypes.guess_type(name)[0])) logger.debug("Found MIME attachment %s" % name) counter += 1 @@ -645,7 +684,8 @@ def object_from_message(message, queue, logger): body = "" if getattr(django_settings, 'HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE', False): - # save message as attachment in case of some complex markup renders wrong + # save message as attachment in case of some complex markup renders + # wrong files.append( SimpleUploadedFile( _("original_message.eml").replace( @@ -660,7 +700,8 @@ def object_from_message(message, queue, logger): smtp_priority = message.get('priority', '') smtp_importance = message.get('importance', '') high_priority_types = {'high', 'important', '1', 'urgent'} - priority = 2 if high_priority_types & {smtp_priority, smtp_importance} else 3 + priority = 2 if high_priority_types & { + smtp_priority, smtp_importance} else 3 payload = { 'body': body, diff --git a/helpdesk/forms.py b/helpdesk/forms.py index 4b74aac2..9aa32a69 100644 --- a/helpdesk/forms.py +++ b/helpdesk/forms.py @@ -37,40 +37,49 @@ class CustomFieldMixin(object): def customfield_to_field(self, field, instanceargs): # Use TextInput widget by default - instanceargs['widget'] = forms.TextInput(attrs={'class': 'form-control'}) + instanceargs['widget'] = forms.TextInput( + attrs={'class': 'form-control'}) # if-elif branches start with special cases if field.data_type == 'varchar': fieldclass = forms.CharField instanceargs['max_length'] = field.max_length elif field.data_type == 'text': fieldclass = forms.CharField - instanceargs['widget'] = forms.Textarea(attrs={'class': 'form-control'}) + instanceargs['widget'] = forms.Textarea( + attrs={'class': 'form-control'}) instanceargs['max_length'] = field.max_length elif field.data_type == 'integer': fieldclass = forms.IntegerField - instanceargs['widget'] = forms.NumberInput(attrs={'class': 'form-control'}) + instanceargs['widget'] = forms.NumberInput( + attrs={'class': 'form-control'}) elif field.data_type == 'decimal': fieldclass = forms.DecimalField instanceargs['decimal_places'] = field.decimal_places instanceargs['max_digits'] = field.max_length - instanceargs['widget'] = forms.NumberInput(attrs={'class': 'form-control'}) + instanceargs['widget'] = forms.NumberInput( + attrs={'class': 'form-control'}) elif field.data_type == 'list': fieldclass = forms.ChoiceField instanceargs['choices'] = field.get_choices() - instanceargs['widget'] = forms.Select(attrs={'class': 'form-control'}) + instanceargs['widget'] = forms.Select( + attrs={'class': 'form-control'}) else: # Try to use the immediate equivalences dictionary try: fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type] # Change widgets for the following classes if fieldclass == forms.DateField: - instanceargs['widget'] = forms.DateInput(attrs={'class': 'form-control date-field'}) + instanceargs['widget'] = forms.DateInput( + attrs={'class': 'form-control date-field'}) elif fieldclass == forms.DateTimeField: - instanceargs['widget'] = forms.DateTimeInput(attrs={'class': 'form-control datetime-field'}) + instanceargs['widget'] = forms.DateTimeInput( + attrs={'class': 'form-control datetime-field'}) elif fieldclass == forms.TimeField: - instanceargs['widget'] = forms.TimeInput(attrs={'class': 'form-control time-field'}) + instanceargs['widget'] = forms.TimeInput( + attrs={'class': 'form-control time-field'}) elif fieldclass == forms.BooleanField: - instanceargs['widget'] = forms.CheckboxInput(attrs={'class': 'form-control'}) + instanceargs['widget'] = forms.CheckboxInput( + attrs={'class': 'form-control'}) except KeyError: # The data_type was not found anywhere @@ -83,10 +92,12 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm): class Meta: model = Ticket - exclude = ('created', 'modified', 'status', 'on_hold', 'resolution', 'last_escalation', 'assigned_to') + exclude = ('created', 'modified', 'status', 'on_hold', + 'resolution', 'last_escalation', 'assigned_to') class Media: - js = ('helpdesk/js/init_due_date.js', 'helpdesk/js/init_datetime_classes.js') + js = ('helpdesk/js/init_due_date.js', + 'helpdesk/js/init_datetime_classes.js') def __init__(self, *args, **kwargs): """ @@ -96,21 +107,28 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm): # Disable and add help_text to the merged_to field on this form self.fields['merged_to'].disabled = True - self.fields['merged_to'].help_text = _('This ticket is merged into the selected ticket.') + self.fields['merged_to'].help_text = _( + 'This ticket is merged into the selected ticket.') for field in CustomField.objects.all(): initial_value = None try: - current_value = TicketCustomFieldValue.objects.get(ticket=self.instance, field=field) + current_value = TicketCustomFieldValue.objects.get( + ticket=self.instance, field=field) initial_value = current_value.value - # Attempt to convert from fixed format string to date/time data type + # Attempt to convert from fixed format string to date/time data + # type if 'datetime' == current_value.field.data_type: - initial_value = datetime.strptime(initial_value, CUSTOMFIELD_DATETIME_FORMAT) + initial_value = datetime.strptime( + initial_value, CUSTOMFIELD_DATETIME_FORMAT) elif 'date' == current_value.field.data_type: - initial_value = datetime.strptime(initial_value, CUSTOMFIELD_DATE_FORMAT) + initial_value = datetime.strptime( + initial_value, CUSTOMFIELD_DATE_FORMAT) elif 'time' == current_value.field.data_type: - initial_value = datetime.strptime(initial_value, CUSTOMFIELD_TIME_FORMAT) - # If it is boolean field, transform the value to a real boolean instead of a string + initial_value = datetime.strptime( + initial_value, CUSTOMFIELD_TIME_FORMAT) + # If it is boolean field, transform the value to a real boolean + # instead of a string elif 'boolean' == current_value.field.data_type: initial_value = 'True' == initial_value except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError): @@ -133,9 +151,11 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm): field_name = field.replace('custom_', '', 1) customfield = CustomField.objects.get(name=field_name) try: - cfv = TicketCustomFieldValue.objects.get(ticket=self.instance, field=customfield) + cfv = TicketCustomFieldValue.objects.get( + ticket=self.instance, field=customfield) except ObjectDoesNotExist: - cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield) + cfv = TicketCustomFieldValue( + ticket=self.instance, field=customfield) cfv.value = convert_value(value) cfv.save() @@ -152,7 +172,8 @@ 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)) + self.fields["ticket"].queryset = Ticket.objects.filter( + status__in=(Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS)) class AbstractTicketForm(CustomFieldMixin, forms.Form): @@ -178,7 +199,8 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form): widget=forms.Textarea(attrs={'class': 'form-control'}), label=_('Description of your issue'), required=True, - help_text=_('Please be as descriptive as possible and include all details'), + help_text=_( + 'Please be as descriptive as possible and include all details'), ) priority = forms.ChoiceField( @@ -187,13 +209,16 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form): required=True, initial=getattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY', '3'), label=_('Priority'), - help_text=_("Please select a priority carefully. If unsure, leave it as '3'."), + help_text=_( + "Please select a priority carefully. If unsure, leave it as '3'."), ) due_date = forms.DateTimeField( - widget=forms.TextInput(attrs={'class': 'form-control', 'autocomplete': 'off'}), + widget=forms.TextInput( + attrs={'class': 'form-control', 'autocomplete': 'off'}), required=False, - input_formats=[CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, '%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"], + input_formats=[CUSTOMFIELD_DATE_FORMAT, + CUSTOMFIELD_DATETIME_FORMAT, '%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"], label=_('Due on'), ) @@ -205,7 +230,8 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form): ) class Media: - js = ('helpdesk/js/init_due_date.js', 'helpdesk/js/init_datetime_classes.js') + js = ('helpdesk/js/init_due_date.js', + 'helpdesk/js/init_datetime_classes.js') def __init__(self, kbcategory=None, *args, **kwargs): super().__init__(*args, **kwargs) @@ -215,7 +241,8 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form): widget=forms.Select(attrs={'class': 'form-control'}), required=False, label=_('Knowledge Base Item'), - choices=[(kbi.pk, kbi.title) for kbi in KBItem.objects.filter(category=kbcategory.pk, enabled=True)], + choices=[(kbi.pk, kbi.title) for kbi in KBItem.objects.filter( + category=kbcategory.pk, enabled=True)], ) def _add_form_custom_fields(self, staff_only_filter=None): @@ -307,7 +334,8 @@ class TicketForm(AbstractTicketForm): submitter_email = forms.EmailField( required=False, label=_('Submitter E-Mail Address'), - widget=forms.TextInput(attrs={'class': 'form-control', 'type': 'email'}), + widget=forms.TextInput( + attrs={'class': 'form-control', 'type': 'email'}), help_text=_('This e-mail address will receive copies of all public ' 'updates to this ticket.'), ) @@ -335,10 +363,13 @@ class TicketForm(AbstractTicketForm): self.fields['queue'].choices = queue_choices if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS: - assignable_users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) + assignable_users = User.objects.filter( + is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) else: - assignable_users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD) - self.fields['assigned_to'].choices = [('', '--------')] + [(u.id, u.get_username()) for u in assignable_users] + assignable_users = User.objects.filter( + is_active=True).order_by(User.USERNAME_FIELD) + self.fields['assigned_to'].choices = [ + ('', '--------')] + [(u.id, u.get_username()) for u in assignable_users] self._add_form_custom_fields() def save(self, user): @@ -380,7 +411,8 @@ class PublicTicketForm(AbstractTicketForm): Ticket Form creation for all users (public-facing). """ submitter_email = forms.EmailField( - widget=forms.TextInput(attrs={'class': 'form-control', 'type': 'email'}), + widget=forms.TextInput( + attrs={'class': 'form-control', 'type': 'email'}), required=True, label=_('Your E-Mail Address'), help_text=_('We will e-mail you when your ticket is updated.'), @@ -406,7 +438,8 @@ class PublicTicketForm(AbstractTicketForm): } for field_name, field_setting_key in field_deletion_table.items(): - has_settings_default_value = getattr(settings, field_setting_key, None) + has_settings_default_value = getattr( + settings, field_setting_key, None) if has_settings_default_value is not None: del self.fields[field_name] @@ -485,9 +518,11 @@ class TicketCCForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(TicketCCForm, self).__init__(*args, **kwargs) if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC: - users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) + users = User.objects.filter( + is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) else: - users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD) + users = User.objects.filter( + is_active=True).order_by(User.USERNAME_FIELD) self.fields['user'].queryset = users @@ -497,9 +532,11 @@ class TicketCCUserForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(TicketCCUserForm, self).__init__(*args, **kwargs) if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC: - users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) + users = User.objects.filter( + is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) else: - users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD) + users = User.objects.filter( + is_active=True).order_by(User.USERNAME_FIELD) self.fields['user'].queryset = users class Meta: @@ -538,8 +575,11 @@ class MultipleTicketSelectForm(forms.Form): if len(tickets) < 2: raise ValidationError(_('Please choose at least 2 tickets.')) if len(tickets) > 4: - raise ValidationError(_('Impossible to merge more than 4 tickets...')) - queues = tickets.order_by('queue').distinct().values_list('queue', flat=True) + raise ValidationError( + _('Impossible to merge more than 4 tickets...')) + queues = tickets.order_by('queue').distinct( + ).values_list('queue', flat=True) if len(queues) != 1: - raise ValidationError(_('All selected tickets must share the same queue in order to be merged.')) + raise ValidationError( + _('All selected tickets must share the same queue in order to be merged.')) return tickets diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 7d2a1c04..911c7a96 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -129,7 +129,8 @@ def text_is_spam(text, request): def process_attachments(followup, attached_files): - max_email_attachment_size = getattr(settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000) + max_email_attachment_size = getattr( + settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000) attachments = [] for attached in attached_files: @@ -152,7 +153,8 @@ def process_attachments(followup, attached_files): if attached.size < max_email_attachment_size: # Only files smaller than 512kb (or as defined in - # settings.HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE) are sent via email. + # settings.HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE) are sent via + # email. attachments.append([filename, att.file]) return attachments diff --git a/helpdesk/management/commands/create_escalation_exclusions.py b/helpdesk/management/commands/create_escalation_exclusions.py index 8801e0f0..9e2148d5 100644 --- a/helpdesk/management/commands/create_escalation_exclusions.py +++ b/helpdesk/management/commands/create_escalation_exclusions.py @@ -66,7 +66,8 @@ class Command(BaseCommand): raise CommandError("Queue %s does not exist." % queue) queues.append(q) - create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues) + create_exclusions(days=days, occurrences=occurrences, + verbose=verbose, queues=queues) day_names = { @@ -90,11 +91,13 @@ def create_exclusions(days, occurrences, verbose, queues): while i < occurrences: if day == workdate.weekday(): if EscalationExclusion.objects.filter(date=workdate).count() == 0: - esc = EscalationExclusion(name='Auto Exclusion for %s' % day_name, date=workdate) + esc = EscalationExclusion( + name='Auto Exclusion for %s' % day_name, date=workdate) esc.save() if verbose: - print("Created exclusion for %s %s" % (day_name, workdate)) + print("Created exclusion for %s %s" % + (day_name, workdate)) for q in queues: esc.queues.add(q) @@ -116,7 +119,8 @@ def usage(): if __name__ == '__main__': # This script can be run from the command-line or via Django's manage.py. try: - opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', ['days=', 'occurrences=', 'verbose', 'queues=']) + opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', [ + 'days=', 'occurrences=', 'verbose', 'queues=']) except getopt.GetoptError: usage() sys.exit(2) @@ -151,4 +155,5 @@ if __name__ == '__main__': sys.exit(2) queues.append(q) - create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues) + create_exclusions(days=days, occurrences=occurrences, + verbose=verbose, queues=queues) diff --git a/helpdesk/management/commands/create_queue_permissions.py b/helpdesk/management/commands/create_queue_permissions.py index fb72f3fe..91f064dc 100644 --- a/helpdesk/management/commands/create_queue_permissions.py +++ b/helpdesk/management/commands/create_queue_permissions.py @@ -55,14 +55,17 @@ class Command(BaseCommand): self.stdout.write("Preparing Queue %s [%s]" % (q.title, q.slug)) if q.permission_name: - self.stdout.write(" .. already has `permission_name=%s`" % q.permission_name) + self.stdout.write( + " .. already has `permission_name=%s`" % q.permission_name) basename = q.permission_name[9:] else: basename = q.generate_permission_name() - self.stdout.write(" .. generated `permission_name=%s`" % q.permission_name) + self.stdout.write( + " .. generated `permission_name=%s`" % q.permission_name) q.save() - self.stdout.write(" .. checking permission codename `%s`" % basename) + self.stdout.write( + " .. checking permission codename `%s`" % basename) try: Permission.objects.create( diff --git a/helpdesk/management/commands/escalate_tickets.py b/helpdesk/management/commands/escalate_tickets.py index 07c9a1c4..020a3b78 100644 --- a/helpdesk/management/commands/escalate_tickets.py +++ b/helpdesk/management/commands/escalate_tickets.py @@ -62,7 +62,8 @@ class Command(BaseCommand): def escalate_tickets(queues, verbose): """ Only include queues with escalation configured """ - queryset = Queue.objects.filter(escalate_days__isnull=False).exclude(escalate_days=0) + queryset = Queue.objects.filter( + escalate_days__isnull=False).exclude(escalate_days=0) if queues: queryset = queryset.filter(slug__in=queues) @@ -143,7 +144,8 @@ def usage(): if __name__ == '__main__': try: - opts, args = getopt.getopt(sys.argv[1:], ['queues=', 'verboseescalation']) + opts, args = getopt.getopt( + sys.argv[1:], ['queues=', 'verboseescalation']) except getopt.GetoptError: usage() sys.exit(2) diff --git a/helpdesk/models.py b/helpdesk/models.py index 5e3eaebb..7ef1b9c9 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -128,7 +128,8 @@ class Queue(models.Model): _('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( @@ -180,7 +181,8 @@ class Queue(models.Model): email_box_type = models.CharField( _('E-Mail Box Type'), max_length=5, - choices=(('pop3', _('POP 3')), ('imap', _('IMAP')), ('local', _('Local Directory'))), + choices=(('pop3', _('POP 3')), ('imap', _('IMAP')), + ('local', _('Local Directory'))), blank=True, null=True, help_text=_('E-Mail server type for creating tickets automatically ' @@ -262,7 +264,8 @@ class Queue(models.Model): email_box_interval = models.IntegerField( _('E-Mail Check Interval'), - help_text=_('How often do you wish to check this mailbox? (in Minutes)'), + help_text=_( + 'How often do you wish to check this mailbox? (in Minutes)'), blank=True, null=True, default='5', @@ -281,7 +284,8 @@ class Queue(models.Model): choices=(('socks4', _('SOCKS4')), ('socks5', _('SOCKS5'))), blank=True, null=True, - help_text=_('SOCKS4 or SOCKS5 allows you to proxy your connections through a SOCKS server.'), + help_text=_( + 'SOCKS4 or SOCKS5 allows you to proxy your connections through a SOCKS server.'), ) socks_proxy_host = models.GenericIPAddressField( @@ -295,7 +299,8 @@ class Queue(models.Model): _('Socks Proxy Port'), blank=True, null=True, - help_text=_('Socks proxy port number. Default: 9150 (default TOR port)'), + help_text=_( + 'Socks proxy port number. Default: 9150 (default TOR port)'), ) logging_type = models.CharField( @@ -356,7 +361,8 @@ class Queue(models.Model): """ if not self.email_address: # must check if given in format "Foo " - default_email = re.match(".*<(?P.*@*.)>", settings.DEFAULT_FROM_EMAIL) + default_email = re.match( + ".*<(?P.*@*.)>", settings.DEFAULT_FROM_EMAIL) if default_email is not None: # already in the right format, so just include it here return u'NO QUEUE EMAIL ADDRESS DEFINED %s' % settings.DEFAULT_FROM_EMAIL @@ -532,7 +538,8 @@ 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( @@ -582,7 +589,8 @@ class Ticket(models.Model): blank=True, null=True, on_delete=models.CASCADE, - verbose_name=_('Knowledge base item the user was viewing when they created this ticket.'), + verbose_name=_( + 'Knowledge base item the user was viewing when they created this ticket.'), ) merged_to = models.ForeignKey( @@ -648,7 +656,8 @@ class Ticket(models.Model): def send(role, recipient): if recipient and recipient not in recipients and role in roles: template, context = roles[role] - send_templated_mail(template, context, recipient, sender=self.queue.from_address, **kwargs) + send_templated_mail( + template, context, recipient, sender=self.queue.from_address, **kwargs) recipients.add(recipient) send('submitter', self.submitter_email) @@ -844,7 +853,8 @@ class Ticket(models.Model): # Ignore if user has no email address return elif not email: - raise ValueError('You must provide at least one parameter to get the email from') + raise ValueError( + 'You must provide at least one parameter to get the email from') # Prepare all emails already into the ticket ticket_emails = [x.display for x in self.ticketcc_set.all()] @@ -1280,7 +1290,8 @@ class EmailTemplate(models.Model): 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( @@ -1329,7 +1340,8 @@ class KBCategory(models.Model): blank=True, null=True, on_delete=models.CASCADE, - verbose_name=_('Default queue when creating a ticket after viewing this category.'), + verbose_name=_( + 'Default queue when creating a ticket after viewing this category.'), ) public = models.BooleanField( @@ -1396,7 +1408,8 @@ 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, ) @@ -1555,7 +1568,8 @@ class UserSettings(models.Model): login_view_ticketlist = models.BooleanField( verbose_name=_('Show Ticket List on Login?'), - help_text=_('Display the ticket list upon login? Otherwise, the dashboard is shown.'), + help_text=_( + 'Display the ticket list upon login? Otherwise, the dashboard is shown.'), default=login_view_ticketlist_default, ) @@ -1570,13 +1584,15 @@ class UserSettings(models.Model): email_on_ticket_assign = models.BooleanField( verbose_name=_('E-mail me when assigned a ticket?'), - help_text=_('If you are assigned a ticket via the web, do you want to receive an e-mail?'), + help_text=_( + 'If you are assigned a ticket via the web, do you want to receive an e-mail?'), default=email_on_ticket_assign_default, ) tickets_per_page = models.IntegerField( verbose_name=_('Number of tickets to show per page'), - help_text=_('How many tickets do you want to see on the Ticket List page?'), + help_text=_( + 'How many tickets do you want to see on the Ticket List page?'), default=tickets_per_page_default, choices=PAGE_SIZES, ) @@ -1611,7 +1627,8 @@ def create_usersettings(sender, instance, created, **kwargs): UserSettings.objects.create(user=instance) -models.signals.post_save.connect(create_usersettings, sender=settings.AUTH_USER_MODEL) +models.signals.post_save.connect( + create_usersettings, sender=settings.AUTH_USER_MODEL) class IgnoreEmail(models.Model): @@ -1851,14 +1868,16 @@ class CustomField(models.Model): ordering = models.IntegerField( _('Ordering'), - help_text=_('Lower numbers are displayed first; higher numbers are listed later'), + help_text=_( + 'Lower numbers are displayed first; higher numbers are listed later'), blank=True, null=True, ) def _choices_as_array(self): valuebuffer = StringIO(self.list_values) - choices = [[item.strip(), item.strip()] for item in valuebuffer.readlines()] + choices = [[item.strip(), item.strip()] + for item in valuebuffer.readlines()] valuebuffer.close() return choices choices_as_array = property(_choices_as_array) @@ -1912,10 +1931,10 @@ class CustomField(models.Model): # Prepare attributes for each types attributes = { - 'label': self.label, - 'help_text': self.help_text, - 'required': self.required, - } + 'label': self.label, + 'help_text': self.help_text, + 'required': self.required, + } if self.data_type in ('varchar', 'text'): attributes['max_length'] = self.max_length if self.data_type == 'text': diff --git a/helpdesk/query.py b/helpdesk/query.py index c347c406..14da72a8 100644 --- a/helpdesk/query.py +++ b/helpdesk/query.py @@ -103,8 +103,10 @@ def get_query_class(): class __Query__: def __init__(self, huser, base64query=None, query_params=None): self.huser = huser - self.params = query_params if query_params else query_from_base64(base64query) - self.base64 = base64query if base64query else query_to_base64(query_params) + self.params = query_params if query_params else query_from_base64( + base64query) + self.base64 = base64query if base64query else query_to_base64( + query_params) self.result = None def get_search_filter_args(self): @@ -128,7 +130,8 @@ class __Query__: """ filter = self.params.get('filtering', {}) filter_or = self.params.get('filtering_or', {}) - queryset = queryset.filter((Q(**filter) | Q(**filter_or)) & self.get_search_filter_args()) + queryset = queryset.filter( + (Q(**filter) | Q(**filter_or)) & self.get_search_filter_args()) sorting = self.params.get('sorting', None) if sorting: sortreverse = self.params.get('sortreverse', None) @@ -191,11 +194,13 @@ class __Query__: 'text': { 'headline': ticket.title + ' - ' + followup.title, 'text': ( - (escape(followup.comment) if followup.comment else _('No text')) + (escape(followup.comment) + if followup.comment else _('No text')) + '
%s' % - (reverse('helpdesk:view', kwargs={'ticket_id': ticket.pk}), _("View ticket")) + (reverse('helpdesk:view', kwargs={ + 'ticket_id': ticket.pk}), _("View ticket")) ), }, 'group': _('Messages'), diff --git a/helpdesk/serializers.py b/helpdesk/serializers.py index ebd5cb30..c4134fa2 100644 --- a/helpdesk/serializers.py +++ b/helpdesk/serializers.py @@ -78,7 +78,8 @@ class FollowUpAttachmentSerializer(serializers.ModelSerializer): class FollowUpSerializer(serializers.ModelSerializer): - followupattachment_set = FollowUpAttachmentSerializer(many=True, read_only=True) + followupattachment_set = FollowUpAttachmentSerializer( + many=True, read_only=True) attachments = serializers.ListField( child=serializers.FileField(), write_only=True, @@ -133,7 +134,8 @@ class TicketSerializer(serializers.ModelSerializer): files = {'attachment': data.pop('attachment', None)} - ticket_form = TicketForm(data=data, files=files, queue_choices=queue_choices) + ticket_form = TicketForm( + data=data, files=files, queue_choices=queue_choices) if ticket_form.is_valid(): ticket = ticket_form.save(user=self.context['request'].user) ticket.set_custom_field_values() diff --git a/helpdesk/settings.py b/helpdesk/settings.py index 4b5fb7bb..2724c01b 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -25,6 +25,9 @@ except AttributeError: HAS_TAG_SUPPORT = False +# Use international timezones +USE_TZ: bool = True + # check for secure cookie support if os.environ.get('SECURE_PROXY_SSL_HEADER'): SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') @@ -43,13 +46,13 @@ HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(settings, # Enable the Dependencies field on ticket view HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(settings, - 'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET', - True) + 'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET', + True) # Enable the Time spent on field on ticket view HELPDESK_ENABLE_TIME_SPENT_ON_TICKET = getattr(settings, - 'HELPDESK_ENABLE_TIME_SPENT_ON_TICKET', - True) + 'HELPDESK_ENABLE_TIME_SPENT_ON_TICKET', + True) # raises a 404 to anon users. It's like it was invisible HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings, @@ -60,10 +63,13 @@ HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings, HELPDESK_KB_ENABLED = getattr(settings, 'HELPDESK_KB_ENABLED', True) # Disable Timeline on ticket list -HELPDESK_TICKETS_TIMELINE_ENABLED = getattr(settings, 'HELPDESK_TICKETS_TIMELINE_ENABLED', True) +HELPDESK_TICKETS_TIMELINE_ENABLED = getattr( + settings, 'HELPDESK_TICKETS_TIMELINE_ENABLED', True) -# show extended navigation by default, to all users, irrespective of staff status? -HELPDESK_NAVIGATION_ENABLED = getattr(settings, 'HELPDESK_NAVIGATION_ENABLED', False) +# 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 @@ -81,7 +87,8 @@ HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG = getattr(settings, ["en", "de", "es", "fr", "it", "ru"]) # show link to 'change password' on 'User Settings' page? -HELPDESK_SHOW_CHANGE_PASSWORD = getattr(settings, 'HELPDESK_SHOW_CHANGE_PASSWORD', False) +HELPDESK_SHOW_CHANGE_PASSWORD = getattr( + settings, 'HELPDESK_SHOW_CHANGE_PASSWORD', False) # allow user to override default layout for 'followups' - work in progress. HELPDESK_FOLLOWUP_MOD = getattr(settings, 'HELPDESK_FOLLOWUP_MOD', False) @@ -93,17 +100,19 @@ HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE = getattr(settings, # URL schemes that are allowed within links ALLOWED_URL_SCHEMES = getattr(settings, 'ALLOWED_URL_SCHEMES', ( - 'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp', + 'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp', )) ############################ # 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) +HELPDESK_VIEW_A_TICKET_PUBLIC = getattr( + settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC', True) # show 'submit a ticket' section on public page? -HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_SUBMIT_A_TICKET_PUBLIC', True) +HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr( + settings, 'HELPDESK_SUBMIT_A_TICKET_PUBLIC', True) # change that to custom class to have extra fields or validation (like captcha) HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr( @@ -134,8 +143,10 @@ CUSTOMFIELD_DATETIME_FORMAT = f"{CUSTOMFIELD_DATE_FORMAT}T%H:%M" ''' options for update_ticket views ''' # allow non-staff users to interact with tickets? -# can be True/False or a callable accepting the active user and returning True if they must be considered helpdesk staff -HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(settings, 'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE', False) +# can be True/False or a callable accepting the active user and returning +# True if they must be considered helpdesk staff +HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr( + settings, 'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE', False) if not (HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE in (True, False) or callable(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE)): warnings.warn( "HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE should be set to either True/False or a callable.", @@ -151,14 +162,18 @@ HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(settings, 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) +# make all updates public by default? this will hide the 'is this update +# public' checkbox +HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr( + settings, 'HELPDESK_UPDATE_PUBLIC_DEFAULT', False) # only show staff users in ticket owner drop-downs -HELPDESK_STAFF_ONLY_TICKET_OWNERS = getattr(settings, 'HELPDESK_STAFF_ONLY_TICKET_OWNERS', False) +HELPDESK_STAFF_ONLY_TICKET_OWNERS = getattr( + settings, 'HELPDESK_STAFF_ONLY_TICKET_OWNERS', False) # only show staff users in ticket cc drop-down -HELPDESK_STAFF_ONLY_TICKET_CC = getattr(settings, 'HELPDESK_STAFF_ONLY_TICKET_CC', False) +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( @@ -170,11 +185,13 @@ if HELPDESK_EMAIL_SUBJECT_TEMPLATE.find("ticket.ticket") < 0: raise ImproperlyConfigured # default fallback locale when queue locale not found -HELPDESK_EMAIL_FALLBACK_LOCALE = getattr(settings, 'HELPDESK_EMAIL_FALLBACK_LOCALE', 'en') +HELPDESK_EMAIL_FALLBACK_LOCALE = getattr( + settings, 'HELPDESK_EMAIL_FALLBACK_LOCALE', 'en') # default maximum email attachment size, in bytes # only attachments smaller than this size will be sent via email -HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr(settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000) +HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr( + settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000) ######################################## @@ -186,7 +203,8 @@ HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr( settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False) # Activate the API endpoint to manage tickets thanks to Django REST Framework -HELPDESK_ACTIVATE_API_ENDPOINT = getattr(settings, 'HELPDESK_ACTIVATE_API_ENDPOINT', False) +HELPDESK_ACTIVATE_API_ENDPOINT = getattr( + settings, 'HELPDESK_ACTIVATE_API_ENDPOINT', False) ################# @@ -201,25 +219,32 @@ QUEUE_EMAIL_BOX_USER = getattr(settings, 'QUEUE_EMAIL_BOX_USER', None) QUEUE_EMAIL_BOX_PASSWORD = getattr(settings, 'QUEUE_EMAIL_BOX_PASSWORD', None) # only process emails with a valid tracking ID? (throws away all other mail) -QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr(settings, 'QUEUE_EMAIL_BOX_UPDATE_ONLY', False) +QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr( + settings, 'QUEUE_EMAIL_BOX_UPDATE_ONLY', False) # only allow users to access queues that they are members of? HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = getattr( settings, 'HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION', False) # use https in the email links -HELPDESK_USE_HTTPS_IN_EMAIL_LINK = getattr(settings, 'HELPDESK_USE_HTTPS_IN_EMAIL_LINK', False) +HELPDESK_USE_HTTPS_IN_EMAIL_LINK = getattr( + settings, 'HELPDESK_USE_HTTPS_IN_EMAIL_LINK', False) -HELPDESK_TEAMS_MODEL = getattr(settings, 'HELPDESK_TEAMS_MODEL', 'pinax_teams.Team') -HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = getattr(settings, 'HELPDESK_TEAMS_MIGRATION_DEPENDENCIES', [('pinax_teams', '0004_auto_20170511_0856')]) -HELPDESK_KBITEM_TEAM_GETTER = getattr(settings, 'HELPDESK_KBITEM_TEAM_GETTER', lambda kbitem: kbitem.team) +HELPDESK_TEAMS_MODEL = getattr( + settings, 'HELPDESK_TEAMS_MODEL', 'pinax_teams.Team') +HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = getattr(settings, 'HELPDESK_TEAMS_MIGRATION_DEPENDENCIES', [ + ('pinax_teams', '0004_auto_20170511_0856')]) +HELPDESK_KBITEM_TEAM_GETTER = getattr( + settings, 'HELPDESK_KBITEM_TEAM_GETTER', lambda kbitem: kbitem.team) # Include all signatures and forwards in the first ticket message if set -# Useful if you get forwards dropped from them while they are useful part of request -HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr(settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False) +# Useful if you get forwards dropped from them while they are useful part +# of request +HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr( + settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False) # If set then we always save incoming emails as .eml attachments # which is quite noisy but very helpful for complicated markup, forwards and so on # (which gets stripped/corrupted otherwise) -HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr(settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False) - +HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr( + settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False) diff --git a/helpdesk/templated_email.py b/helpdesk/templated_email.py index 4b20fc83..2fc83973 100644 --- a/helpdesk/templated_email.py +++ b/helpdesk/templated_email.py @@ -58,12 +58,15 @@ def send_templated_mail(template_name, locale = context['queue'].get('locale') or HELPDESK_EMAIL_FALLBACK_LOCALE try: - t = EmailTemplate.objects.get(template_name__iexact=template_name, locale=locale) + t = EmailTemplate.objects.get( + template_name__iexact=template_name, locale=locale) except EmailTemplate.DoesNotExist: try: - t = EmailTemplate.objects.get(template_name__iexact=template_name, locale__isnull=True) + 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) + logger.warning( + 'template "%s" does not exist, no mail sent', template_name) return # just ignore if template doesn't exist subject_part = from_string( @@ -77,10 +80,12 @@ def send_templated_mail(template_name, "%s\n\n{%% include '%s' %%}" % (t.plain_text, footer_file) ).render(context) - email_html_base_file = os.path.join('helpdesk', locale, 'email_html_base.html') + email_html_base_file = os.path.join( + 'helpdesk', locale, 'email_html_base.html') # keep new lines in html emails if 'comment' in context: - context['comment'] = mark_safe(context['comment'].replace('\r\n', '
')) + context['comment'] = mark_safe( + context['comment'].replace('\r\n', '
')) html_part = from_string( "{%% extends '%s' %%}" @@ -112,7 +117,8 @@ def send_templated_mail(template_name, try: return msg.send() except SMTPException as e: - logger.exception('SMTPException raised while sending email to {}'.format(recipients)) + logger.exception( + 'SMTPException raised while sending email to {}'.format(recipients)) if not fail_silently: raise e return 0 diff --git a/helpdesk/templatetags/helpdesk_staff.py b/helpdesk/templatetags/helpdesk_staff.py index ad916264..ab1ea3cf 100644 --- a/helpdesk/templatetags/helpdesk_staff.py +++ b/helpdesk/templatetags/helpdesk_staff.py @@ -19,4 +19,5 @@ def helpdesk_staff(user): try: return is_helpdesk_staff(user) except Exception: - logger.exception("'helpdesk_staff' template tag (django-helpdesk) crashed") + logger.exception( + "'helpdesk_staff' template tag (django-helpdesk) crashed") diff --git a/helpdesk/templatetags/helpdesk_util.py b/helpdesk/templatetags/helpdesk_util.py index 3ad345d4..b096824c 100644 --- a/helpdesk/templatetags/helpdesk_util.py +++ b/helpdesk/templatetags/helpdesk_util.py @@ -22,13 +22,16 @@ def datetime_string_format(value): :return: String - reformatted to default datetime, date, or time string if received in one of the expected formats """ try: - new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_DATETIME_FORMAT), settings.DATETIME_FORMAT) + new_value = date_filter(datetime.strptime( + value, CUSTOMFIELD_DATETIME_FORMAT), settings.DATETIME_FORMAT) except (TypeError, ValueError): try: - new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT) + new_value = date_filter(datetime.strptime( + value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT) except (TypeError, ValueError): try: - new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_TIME_FORMAT), settings.TIME_FORMAT) + new_value = date_filter(datetime.strptime( + value, CUSTOMFIELD_TIME_FORMAT), settings.TIME_FORMAT) except (TypeError, ValueError): # If NoneType return empty string, else return original value new_value = "" if value is None else value diff --git a/helpdesk/tests/test_api.py b/helpdesk/tests/test_api.py index ab9a146c..45c4bcfe 100644 --- a/helpdesk/tests/test_api.py +++ b/helpdesk/tests/test_api.py @@ -72,7 +72,8 @@ class TicketTest(APITestCase): self.assertEqual(response.status_code, HTTP_201_CREATED) created_ticket = Ticket.objects.get() self.assertEqual(created_ticket.title, 'Test title') - self.assertEqual(created_ticket.description, 'Test description\nMulti lines') + self.assertEqual(created_ticket.description, + 'Test description\nMulti lines') self.assertEqual(created_ticket.submitter_email, 'test@mail.com') self.assertEqual(created_ticket.priority, 4) self.assertEqual(created_ticket.followup_set.count(), 1) @@ -80,16 +81,20 @@ class TicketTest(APITestCase): def test_create_api_ticket_with_basic_auth(self): username = 'admin' password = 'admin' - User.objects.create_user(username=username, password=password, is_staff=True) + User.objects.create_user( + username=username, password=password, is_staff=True) test_user = User.objects.create_user(username='test') - merge_ticket = Ticket.objects.create(queue=self.queue, title='merge ticket') + merge_ticket = Ticket.objects.create( + queue=self.queue, title='merge ticket') # Generate base64 credentials string credentials = f"{username}:{password}" - base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING) + base64_credentials = base64.b64encode(credentials.encode( + HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING) - self.client.credentials(HTTP_AUTHORIZATION=f"Basic {base64_credentials}") + self.client.credentials( + HTTP_AUTHORIZATION=f"Basic {base64_credentials}") response = self.client.post( '/api/tickets/', { @@ -111,21 +116,27 @@ class TicketTest(APITestCase): created_ticket = Ticket.objects.last() self.assertEqual(created_ticket.title, 'Title') self.assertEqual(created_ticket.description, 'Description') - self.assertIsNone(created_ticket.resolution) # resolution can not be set on creation + # resolution can not be set on creation + self.assertIsNone(created_ticket.resolution) self.assertEqual(created_ticket.assigned_to, test_user) self.assertEqual(created_ticket.submitter_email, 'test@mail.com') self.assertEqual(created_ticket.priority, 1) - self.assertFalse(created_ticket.on_hold) # on_hold is False on creation - self.assertEqual(created_ticket.status, Ticket.OPEN_STATUS) # status is always open on creation + # on_hold is False on creation + self.assertFalse(created_ticket.on_hold) + # status is always open on creation + self.assertEqual(created_ticket.status, Ticket.OPEN_STATUS) self.assertEqual(created_ticket.due_date, self.due_date) - self.assertIsNone(created_ticket.merged_to) # merged_to can not be set on creation + # merged_to can not be set on creation + self.assertIsNone(created_ticket.merged_to) def test_edit_api_ticket(self): staff_user = User.objects.create_user(username='admin', is_staff=True) - test_ticket = Ticket.objects.create(queue=self.queue, title='Test ticket') + test_ticket = Ticket.objects.create( + queue=self.queue, title='Test ticket') test_user = User.objects.create_user(username='test') - merge_ticket = Ticket.objects.create(queue=self.queue, title='merge ticket') + merge_ticket = Ticket.objects.create( + queue=self.queue, title='merge ticket') self.client.force_authenticate(staff_user) response = self.client.put( @@ -160,7 +171,8 @@ class TicketTest(APITestCase): def test_partial_edit_api_ticket(self): staff_user = User.objects.create_user(username='admin', is_staff=True) - test_ticket = Ticket.objects.create(queue=self.queue, title='Test ticket') + test_ticket = Ticket.objects.create( + queue=self.queue, title='Test ticket') self.client.force_authenticate(staff_user) response = self.client.patch( @@ -176,7 +188,8 @@ class TicketTest(APITestCase): def test_delete_api_ticket(self): staff_user = User.objects.create_user(username='admin', is_staff=True) - test_ticket = Ticket.objects.create(queue=self.queue, title='Test ticket') + test_ticket = Ticket.objects.create( + queue=self.queue, title='Test ticket') self.client.force_authenticate(staff_user) response = self.client.delete('/api/tickets/%d/' % test_ticket.id) self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) @@ -200,7 +213,8 @@ class TicketTest(APITestCase): Blue Red Yellow''' - CustomField.objects.create(name=field_type, label=field_display, data_type=field_type, **extra_data) + CustomField.objects.create( + name=field_type, label=field_display, data_type=field_type, **extra_data) staff_user = User.objects.create_user(username='test', is_staff=True) self.client.force_authenticate(staff_user) @@ -214,7 +228,8 @@ class TicketTest(APITestCase): 'priority': 4 }) self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'custom_integer': [ErrorDetail(string='This field is required.', code='required')]}) + self.assertEqual(response.data, {'custom_integer': [ErrorDetail( + string='This field is required.', code='required')]}) # Test creation with custom field values response = self.client.post('/api/tickets/', { @@ -283,7 +298,8 @@ class TicketTest(APITestCase): def test_create_api_ticket_with_attachment(self): staff_user = User.objects.create_user(username='test', is_staff=True) self.client.force_authenticate(staff_user) - test_file = SimpleUploadedFile('file.jpg', b'file_content', content_type='image/jpg') + test_file = SimpleUploadedFile( + 'file.jpg', b'file_content', content_type='image/jpg') response = self.client.post('/api/tickets/', { 'queue': self.queue.id, 'title': 'Test title', @@ -295,11 +311,13 @@ class TicketTest(APITestCase): self.assertEqual(response.status_code, HTTP_201_CREATED) created_ticket = Ticket.objects.get() self.assertEqual(created_ticket.title, 'Test title') - self.assertEqual(created_ticket.description, 'Test description\nMulti lines') + self.assertEqual(created_ticket.description, + 'Test description\nMulti lines') self.assertEqual(created_ticket.submitter_email, 'test@mail.com') self.assertEqual(created_ticket.priority, 4) self.assertEqual(created_ticket.followup_set.count(), 1) - self.assertEqual(created_ticket.followup_set.get().followupattachment_set.count(), 1) + self.assertEqual(created_ticket.followup_set.get( + ).followupattachment_set.count(), 1) attachment = created_ticket.followup_set.get().followupattachment_set.get() self.assertEqual( attachment.file.name, @@ -310,8 +328,10 @@ class TicketTest(APITestCase): staff_user = User.objects.create_user(username='test', is_staff=True) self.client.force_authenticate(staff_user) ticket = Ticket.objects.create(queue=self.queue, title='Test') - test_file_1 = SimpleUploadedFile('file.jpg', b'file_content', content_type='image/jpg') - test_file_2 = SimpleUploadedFile('doc.pdf', b'Doc content', content_type='application/pdf') + test_file_1 = SimpleUploadedFile( + 'file.jpg', b'file_content', content_type='image/jpg') + test_file_2 = SimpleUploadedFile( + 'doc.pdf', b'Doc content', content_type='application/pdf') response = self.client.post('/api/followups/', { 'ticket': ticket.id, @@ -327,7 +347,11 @@ class TicketTest(APITestCase): self.assertEqual(created_followup.title, 'Test') self.assertEqual(created_followup.comment, 'Test answer\nMulti lines') self.assertEqual(created_followup.followupattachment_set.count(), 2) - self.assertEqual(created_followup.followupattachment_set.first().filename, 'doc.pdf') - self.assertEqual(created_followup.followupattachment_set.first().mime_type, 'application/pdf') - self.assertEqual(created_followup.followupattachment_set.last().filename, 'file.jpg') - self.assertEqual(created_followup.followupattachment_set.last().mime_type, 'image/jpg') + self.assertEqual( + created_followup.followupattachment_set.first().filename, 'doc.pdf') + self.assertEqual( + created_followup.followupattachment_set.first().mime_type, 'application/pdf') + self.assertEqual( + created_followup.followupattachment_set.last().filename, 'file.jpg') + self.assertEqual( + created_followup.followupattachment_set.last().mime_type, 'image/jpg') diff --git a/helpdesk/tests/test_attachments.py b/helpdesk/tests/test_attachments.py index 6c91cb7b..46e7151d 100644 --- a/helpdesk/tests/test_attachments.py +++ b/helpdesk/tests/test_attachments.py @@ -47,7 +47,8 @@ class AttachmentIntegrationTests(TestCase): } def test_create_pub_ticket_with_attachment(self): - test_file = SimpleUploadedFile('test_att.txt', b'attached file content', 'text/plain') + test_file = SimpleUploadedFile( + 'test_att.txt', b'attached file content', 'text/plain') post_data = self.ticket_data.copy() post_data.update({ 'queue': self.queue_public.id, @@ -55,17 +56,20 @@ class AttachmentIntegrationTests(TestCase): }) # Ensure ticket form submits with attachment successfully - response = self.client.post(reverse('helpdesk:home'), post_data, follow=True) + response = self.client.post( + reverse('helpdesk:home'), post_data, follow=True) self.assertContains(response, test_file.name) # Ensure attachment is available with correct content - att = models.FollowUpAttachment.objects.get(followup__ticket=response.context['ticket']) + att = models.FollowUpAttachment.objects.get( + followup__ticket=response.context['ticket']) with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk: disk_content = file_on_disk.read() self.assertEqual(disk_content, 'attached file content') def test_create_pub_ticket_with_attachment_utf8(self): - test_file = SimpleUploadedFile('ß°äöü.txt', 'โจ'.encode('utf-8'), 'text/utf-8') + test_file = SimpleUploadedFile( + 'ß°äöü.txt', 'โจ'.encode('utf-8'), 'text/utf-8') post_data = self.ticket_data.copy() post_data.update({ 'queue': self.queue_public.id, @@ -73,11 +77,13 @@ class AttachmentIntegrationTests(TestCase): }) # Ensure ticket form submits with attachment successfully - response = self.client.post(reverse('helpdesk:home'), post_data, follow=True) + response = self.client.post( + reverse('helpdesk:home'), post_data, follow=True) self.assertContains(response, test_file.name) # Ensure attachment is available with correct content - att = models.FollowUpAttachment.objects.get(followup__ticket=response.context['ticket']) + att = models.FollowUpAttachment.objects.get( + followup__ticket=response.context['ticket']) with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk: disk_content = smart_str(file_on_disk.read(), 'utf-8') self.assertEqual(disk_content, 'โจ') @@ -105,7 +111,8 @@ class AttachmentUnitTests(TestCase): @skip("Rework with model relocation") def test_unicode_attachment_filename(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save): """ check utf-8 data is parsed correctly """ - filename, fileobj = lib.process_attachments(self.follow_up, [self.test_file])[0] + filename, fileobj = lib.process_attachments( + self.follow_up, [self.test_file])[0] mock_att_save.assert_called_with( file=self.test_file, filename=self.file_attrs['filename'], @@ -154,8 +161,10 @@ class AttachmentUnitTests(TestCase): @override_settings(MEDIA_ROOT=MEDIA_DIR) def test_unicode_filename_to_filesystem(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save): """ don't mock saving to filesystem to test file renames caused by storage layer """ - filename, fileobj = lib.process_attachments(self.follow_up, [self.test_file])[0] - # Attachment object was zeroth positional arg (i.e. self) of att.save call + filename, fileobj = lib.process_attachments( + self.follow_up, [self.test_file])[0] + # Attachment object was zeroth positional arg (i.e. self) of att.save + # call attachment_obj = mock_att_save.return_value mock_att_save.assert_called_once_with(attachment_obj) diff --git a/helpdesk/tests/test_get_email.py b/helpdesk/tests/test_get_email.py index 07df3348..f1cf010e 100644 --- a/helpdesk/tests/test_get_email.py +++ b/helpdesk/tests/test_get_email.py @@ -37,7 +37,8 @@ class GetEmailCommonTests(TestCase): # tests correct syntax for command line option def test_get_email_quiet_option(self): """Test quiet option is properly propagated""" - # Test get_email with quiet set to True and also False, and verify handle receives quiet option set properly + # Test get_email with quiet set to True and also False, and verify + # handle receives quiet option set properly for quiet_test_value in [True, False]: with mock.patch.object(Command, 'handle', return_value=None) as mocked_handle: call_command('get_email', quiet=quiet_test_value) @@ -52,7 +53,8 @@ class GetEmailCommonTests(TestCase): """ with open(os.path.join(THIS_DIR, "test_files/blank-body-with-attachment.eml")) as fd: test_email = fd.read() - ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) + ticket = helpdesk.email.object_from_message( + test_email, self.queue_public, self.logger) # title got truncated because of max_lengh of the model.title field assert ticket.title == ( @@ -68,16 +70,19 @@ class GetEmailCommonTests(TestCase): """ with open(os.path.join(THIS_DIR, "test_files/quoted_printable.eml")) as fd: test_email = fd.read() - ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) + ticket = helpdesk.email.object_from_message( + test_email, self.queue_public, self.logger) self.assertEqual(ticket.title, "Český test") - self.assertEqual(ticket.description, "Tohle je test českých písmen odeslaných z gmailu.") + self.assertEqual(ticket.description, + "Tohle je test českých písmen odeslaných z gmailu.") followups = FollowUp.objects.filter(ticket=ticket) self.assertEqual(len(followups), 1) followup = followups[0] attachments = FollowUpAttachment.objects.filter(followup=followup) self.assertEqual(len(attachments), 1) attachment = attachments[0] - self.assertIn('
Tohle je test českých písmen odeslaných z gmailu.
\n', attachment.file.read().decode("utf-8")) + self.assertIn('
Tohle je test českých písmen odeslaných z gmailu.
\n', + attachment.file.read().decode("utf-8")) def test_email_with_8bit_encoding_and_utf_8(self): """ @@ -86,7 +91,8 @@ class GetEmailCommonTests(TestCase): """ with open(os.path.join(THIS_DIR, "test_files/all-special-chars.eml")) as fd: test_email = fd.read() - ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) + ticket = helpdesk.email.object_from_message( + test_email, self.queue_public, self.logger) self.assertEqual(ticket.title, "Testovácí email") self.assertEqual(ticket.description, "íářčšáíéřášč") @@ -98,14 +104,17 @@ class GetEmailCommonTests(TestCase): """ with open(os.path.join(THIS_DIR, "test_files/utf-nondecodable.eml")) as fd: test_email = fd.read() - ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) - self.assertEqual(ticket.title, "Fwd: Cyklozaměstnavatel - změna vyhodnocení") + ticket = helpdesk.email.object_from_message( + test_email, self.queue_public, self.logger) + self.assertEqual( + ticket.title, "Fwd: Cyklozaměstnavatel - změna vyhodnocení") self.assertIn("prosazuje lepší", ticket.description) followups = FollowUp.objects.filter(ticket=ticket) followup = followups[0] attachments = FollowUpAttachment.objects.filter(followup=followup) attachment = attachments[0] - self.assertIn('prosazuje lepší', attachment.file.read().decode("utf-8")) + self.assertIn('prosazuje lepší', + attachment.file.read().decode("utf-8")) @override_settings(HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL=True) def test_email_with_forwarded_message(self): @@ -114,12 +123,15 @@ class GetEmailCommonTests(TestCase): """ with open(os.path.join(THIS_DIR, "test_files/forwarded-message.eml")) as fd: test_email = fd.read() - ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) - self.assertEqual(ticket.title, "Test with original message from GitHub") + ticket = helpdesk.email.object_from_message( + test_email, self.queue_public, self.logger) + self.assertEqual( + ticket.title, "Test with original message from GitHub") self.assertIn("This is email body", ticket.description) assert "Hello there!" not in ticket.description, ticket.description assert FollowUp.objects.filter(ticket=ticket).count() == 1 - assert "Hello there!" in FollowUp.objects.filter(ticket=ticket).first().comment + assert "Hello there!" in FollowUp.objects.filter( + ticket=ticket).first().comment class GetEmailParametricTemplate(object): @@ -160,11 +172,13 @@ class GetEmailParametricTemplate(object): For each email source supported, we mock the backend to provide authentically formatted responses containing our test data.""" - # example email text from Django docs: https://docs.djangoproject.com/en/1.10/ref/unicode/ + # example email text from Django docs: + # https://docs.djangoproject.com/en/1.10/ref/unicode/ test_email_from = "Arnbjörg Ráðormsdóttir " test_email_subject = "My visit to Sør-Trøndelag" test_email_body = "Unicode helpdesk comment with an s-hat (ŝ) via email." - test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + "\nSubject: " + test_email_subject + "\n\n" + test_email_body + test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + \ + "\nSubject: " + test_email_subject + "\n\n" + test_email_body test_mail_len = len(test_email) if self.socks: @@ -184,38 +198,51 @@ class GetEmailParametricTemplate(object): call_command('get_email') - mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/') - mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1') - mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename2') + mocked_listdir.assert_called_with( + '/var/lib/mail/helpdesk/') + mocked_isfile.assert_any_call( + '/var/lib/mail/helpdesk/filename1') + mocked_isfile.assert_any_call( + '/var/lib/mail/helpdesk/filename2') elif self.method == 'pop3': - # mock poplib.POP3's list and retr methods to provide responses as per RFC 1939 + # mock poplib.POP3's list and retr methods to provide responses + # as per RFC 1939 pop3_emails = { '1': ("+OK", test_email.split('\n')), '2': ("+OK", test_email.split('\n')), } - pop3_mail_list = ("+OK 2 messages", ("1 %d" % test_mail_len, "2 %d" % test_mail_len)) + pop3_mail_list = ("+OK 2 messages", ("1 %d" % + test_mail_len, "2 %d" % test_mail_len)) mocked_poplib_server = mock.Mock() - mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list) - mocked_poplib_server.retr = mock.Mock(side_effect=lambda x: pop3_emails[x]) + mocked_poplib_server.list = mock.Mock( + return_value=pop3_mail_list) + mocked_poplib_server.retr = mock.Mock( + side_effect=lambda x: pop3_emails[x]) with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: - mocked_poplib.POP3 = mock.Mock(return_value=mocked_poplib_server) + mocked_poplib.POP3 = mock.Mock( + return_value=mocked_poplib_server) call_command('get_email') elif self.method == 'imap': - # mock imaplib.IMAP4's search and fetch methods with responses from RFC 3501 + # mock imaplib.IMAP4's search and fetch methods with responses + # from RFC 3501 imap_emails = { "1": ("OK", (("1", test_email),)), "2": ("OK", (("2", test_email),)), } imap_mail_list = ("OK", ("1 2",)) mocked_imaplib_server = mock.Mock() - mocked_imaplib_server.search = mock.Mock(return_value=imap_mail_list) + mocked_imaplib_server.search = mock.Mock( + return_value=imap_mail_list) - # we ignore the second arg as the data item/mime-part is constant (RFC822) - mocked_imaplib_server.fetch = mock.Mock(side_effect=lambda x, _: imap_emails[x]) + # we ignore the second arg as the data item/mime-part is + # constant (RFC822) + mocked_imaplib_server.fetch = mock.Mock( + side_effect=lambda x, _: imap_emails[x]) with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: - mocked_imaplib.IMAP4 = mock.Mock(return_value=mocked_imaplib_server) + mocked_imaplib.IMAP4 = mock.Mock( + return_value=mocked_imaplib_server) call_command('get_email') ticket1 = get_object_or_404(Ticket, pk=1) @@ -232,11 +259,13 @@ class GetEmailParametricTemplate(object): """Tests correctly decoding mail headers when a comma is encoded into UTF-8. See bug report #832.""" - # example email text from Django docs: https://docs.djangoproject.com/en/1.10/ref/unicode/ + # example email text from Django docs: + # https://docs.djangoproject.com/en/1.10/ref/unicode/ test_email_from = "Bernard-Bouissières, Benjamin " test_email_subject = "Commas in From lines" test_email_body = "Testing commas in from email UTF-8." - test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + "\nSubject: " + test_email_subject + "\n\n" + test_email_body + test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + \ + "\nSubject: " + test_email_subject + "\n\n" + test_email_body test_mail_len = len(test_email) if self.socks: @@ -256,38 +285,51 @@ class GetEmailParametricTemplate(object): call_command('get_email') - mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/') - mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1') - mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename2') + mocked_listdir.assert_called_with( + '/var/lib/mail/helpdesk/') + mocked_isfile.assert_any_call( + '/var/lib/mail/helpdesk/filename1') + mocked_isfile.assert_any_call( + '/var/lib/mail/helpdesk/filename2') elif self.method == 'pop3': - # mock poplib.POP3's list and retr methods to provide responses as per RFC 1939 + # mock poplib.POP3's list and retr methods to provide responses + # as per RFC 1939 pop3_emails = { '1': ("+OK", test_email.split('\n')), '2': ("+OK", test_email.split('\n')), } - pop3_mail_list = ("+OK 2 messages", ("1 %d" % test_mail_len, "2 %d" % test_mail_len)) + pop3_mail_list = ("+OK 2 messages", ("1 %d" % + test_mail_len, "2 %d" % test_mail_len)) mocked_poplib_server = mock.Mock() - mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list) - mocked_poplib_server.retr = mock.Mock(side_effect=lambda x: pop3_emails[x]) + mocked_poplib_server.list = mock.Mock( + return_value=pop3_mail_list) + mocked_poplib_server.retr = mock.Mock( + side_effect=lambda x: pop3_emails[x]) with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: - mocked_poplib.POP3 = mock.Mock(return_value=mocked_poplib_server) + mocked_poplib.POP3 = mock.Mock( + return_value=mocked_poplib_server) call_command('get_email') elif self.method == 'imap': - # mock imaplib.IMAP4's search and fetch methods with responses from RFC 3501 + # mock imaplib.IMAP4's search and fetch methods with responses + # from RFC 3501 imap_emails = { "1": ("OK", (("1", test_email),)), "2": ("OK", (("2", test_email),)), } imap_mail_list = ("OK", ("1 2",)) mocked_imaplib_server = mock.Mock() - mocked_imaplib_server.search = mock.Mock(return_value=imap_mail_list) + mocked_imaplib_server.search = mock.Mock( + return_value=imap_mail_list) - # we ignore the second arg as the data item/mime-part is constant (RFC822) - mocked_imaplib_server.fetch = mock.Mock(side_effect=lambda x, _: imap_emails[x]) + # we ignore the second arg as the data item/mime-part is + # constant (RFC822) + mocked_imaplib_server.fetch = mock.Mock( + side_effect=lambda x, _: imap_emails[x]) with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: - mocked_imaplib.IMAP4 = mock.Mock(return_value=mocked_imaplib_server) + mocked_imaplib.IMAP4 = mock.Mock( + return_value=mocked_imaplib_server) call_command('get_email') ticket1 = get_object_or_404(Ticket, pk=1) @@ -308,11 +350,13 @@ class GetEmailParametricTemplate(object): For each email source supported, we mock the backend to provide authentically formatted responses containing our test data.""" - # example email text from Django docs: https://docs.djangoproject.com/en/1.10/ref/unicode/ + # example email text from Django docs: + # https://docs.djangoproject.com/en/1.10/ref/unicode/ test_email_from = "Arnbjörg Ráðormsdóttir " test_email_subject = "My visit to Sør-Trøndelag" test_email_body = "Reporting some issue with the template tag: {% if helpdesk %}." - test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + "\nSubject: " + test_email_subject + "\n\n" + test_email_body + test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + \ + "\nSubject: " + test_email_subject + "\n\n" + test_email_body test_mail_len = len(test_email) if self.socks: @@ -332,38 +376,51 @@ class GetEmailParametricTemplate(object): call_command('get_email') - mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/') - mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1') - mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename2') + mocked_listdir.assert_called_with( + '/var/lib/mail/helpdesk/') + mocked_isfile.assert_any_call( + '/var/lib/mail/helpdesk/filename1') + mocked_isfile.assert_any_call( + '/var/lib/mail/helpdesk/filename2') elif self.method == 'pop3': - # mock poplib.POP3's list and retr methods to provide responses as per RFC 1939 + # mock poplib.POP3's list and retr methods to provide responses + # as per RFC 1939 pop3_emails = { '1': ("+OK", test_email.split('\n')), '2': ("+OK", test_email.split('\n')), } - pop3_mail_list = ("+OK 2 messages", ("1 %d" % test_mail_len, "2 %d" % test_mail_len)) + pop3_mail_list = ("+OK 2 messages", ("1 %d" % + test_mail_len, "2 %d" % test_mail_len)) mocked_poplib_server = mock.Mock() - mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list) - mocked_poplib_server.retr = mock.Mock(side_effect=lambda x: pop3_emails[x]) + mocked_poplib_server.list = mock.Mock( + return_value=pop3_mail_list) + mocked_poplib_server.retr = mock.Mock( + side_effect=lambda x: pop3_emails[x]) with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: - mocked_poplib.POP3 = mock.Mock(return_value=mocked_poplib_server) + mocked_poplib.POP3 = mock.Mock( + return_value=mocked_poplib_server) call_command('get_email') elif self.method == 'imap': - # mock imaplib.IMAP4's search and fetch methods with responses from RFC 3501 + # mock imaplib.IMAP4's search and fetch methods with responses + # from RFC 3501 imap_emails = { "1": ("OK", (("1", test_email),)), "2": ("OK", (("2", test_email),)), } imap_mail_list = ("OK", ("1 2",)) mocked_imaplib_server = mock.Mock() - mocked_imaplib_server.search = mock.Mock(return_value=imap_mail_list) + mocked_imaplib_server.search = mock.Mock( + return_value=imap_mail_list) - # we ignore the second arg as the data item/mime-part is constant (RFC822) - mocked_imaplib_server.fetch = mock.Mock(side_effect=lambda x, _: imap_emails[x]) + # we ignore the second arg as the data item/mime-part is + # constant (RFC822) + mocked_imaplib_server.fetch = mock.Mock( + side_effect=lambda x, _: imap_emails[x]) with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: - mocked_imaplib.IMAP4 = mock.Mock(return_value=mocked_imaplib_server) + mocked_imaplib.IMAP4 = mock.Mock( + return_value=mocked_imaplib_server) call_command('get_email') ticket1 = get_object_or_404(Ticket, pk=1) @@ -382,7 +439,8 @@ class GetEmailParametricTemplate(object): For each email source supported, we mock the backend to provide authentically formatted responses containing our test data.""" - # example email text from Python docs: https://docs.python.org/3/library/email-examples.html + # example email text from Python docs: + # https://docs.python.org/3/library/email-examples.html from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText @@ -396,7 +454,8 @@ class GetEmailParametricTemplate(object): cc = cc_one + ", " + cc_two subject = "Link" - # Create message container - the correct MIME type is multipart/alternative. + # Create message container - the correct MIME type is + # multipart/alternative. msg = MIMEMultipart('alternative') msg['Subject'] = subject msg['From'] = me @@ -446,38 +505,51 @@ class GetEmailParametricTemplate(object): call_command('get_email') - mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/') - mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1') - mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename2') + mocked_listdir.assert_called_with( + '/var/lib/mail/helpdesk/') + mocked_isfile.assert_any_call( + '/var/lib/mail/helpdesk/filename1') + mocked_isfile.assert_any_call( + '/var/lib/mail/helpdesk/filename2') elif self.method == 'pop3': - # mock poplib.POP3's list and retr methods to provide responses as per RFC 1939 + # mock poplib.POP3's list and retr methods to provide responses + # as per RFC 1939 pop3_emails = { '1': ("+OK", msg.as_string().split('\n')), '2': ("+OK", msg.as_string().split('\n')), } - pop3_mail_list = ("+OK 2 messages", ("1 %d" % test_mail_len, "2 %d" % test_mail_len)) + pop3_mail_list = ("+OK 2 messages", ("1 %d" % + test_mail_len, "2 %d" % test_mail_len)) mocked_poplib_server = mock.Mock() - mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list) - mocked_poplib_server.retr = mock.Mock(side_effect=lambda x: pop3_emails[x]) + mocked_poplib_server.list = mock.Mock( + return_value=pop3_mail_list) + mocked_poplib_server.retr = mock.Mock( + side_effect=lambda x: pop3_emails[x]) with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: - mocked_poplib.POP3 = mock.Mock(return_value=mocked_poplib_server) + mocked_poplib.POP3 = mock.Mock( + return_value=mocked_poplib_server) call_command('get_email') elif self.method == 'imap': - # mock imaplib.IMAP4's search and fetch methods with responses from RFC 3501 + # mock imaplib.IMAP4's search and fetch methods with responses + # from RFC 3501 imap_emails = { "1": ("OK", (("1", msg.as_string()),)), "2": ("OK", (("2", msg.as_string()),)), } imap_mail_list = ("OK", ("1 2",)) mocked_imaplib_server = mock.Mock() - mocked_imaplib_server.search = mock.Mock(return_value=imap_mail_list) + mocked_imaplib_server.search = mock.Mock( + return_value=imap_mail_list) - # we ignore the second arg as the data item/mime-part is constant (RFC822) - mocked_imaplib_server.fetch = mock.Mock(side_effect=lambda x, _: imap_emails[x]) + # we ignore the second arg as the data item/mime-part is + # constant (RFC822) + mocked_imaplib_server.fetch = mock.Mock( + side_effect=lambda x, _: imap_emails[x]) with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: - mocked_imaplib.IMAP4 = mock.Mock(return_value=mocked_imaplib_server) + mocked_imaplib.IMAP4 = mock.Mock( + return_value=mocked_imaplib_server) call_command('get_email') ticket1 = get_object_or_404(Ticket, pk=1) @@ -537,41 +609,54 @@ class GetEmailParametricTemplate(object): call_command('get_email') - mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/') - mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1') + mocked_listdir.assert_called_with( + '/var/lib/mail/helpdesk/') + mocked_isfile.assert_any_call( + '/var/lib/mail/helpdesk/filename1') elif self.method == 'pop3': - # mock poplib.POP3's list and retr methods to provide responses as per RFC 1939 + # mock poplib.POP3's list and retr methods to provide responses + # as per RFC 1939 pop3_emails = { '1': ("+OK", test_email.split('\n')), } pop3_mail_list = ("+OK 1 message", ("1 %d" % test_mail_len)) mocked_poplib_server = mock.Mock() - mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list) - mocked_poplib_server.retr = mock.Mock(side_effect=lambda x: pop3_emails['1']) + mocked_poplib_server.list = mock.Mock( + return_value=pop3_mail_list) + mocked_poplib_server.retr = mock.Mock( + side_effect=lambda x: pop3_emails['1']) with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: - mocked_poplib.POP3 = mock.Mock(return_value=mocked_poplib_server) + mocked_poplib.POP3 = mock.Mock( + return_value=mocked_poplib_server) call_command('get_email') elif self.method == 'imap': - # mock imaplib.IMAP4's search and fetch methods with responses from RFC 3501 + # mock imaplib.IMAP4's search and fetch methods with responses + # from RFC 3501 imap_emails = { "1": ("OK", (("1", test_email),)), } imap_mail_list = ("OK", ("1",)) mocked_imaplib_server = mock.Mock() - mocked_imaplib_server.search = mock.Mock(return_value=imap_mail_list) + mocked_imaplib_server.search = mock.Mock( + return_value=imap_mail_list) - # we ignore the second arg as the data item/mime-part is constant (RFC822) - mocked_imaplib_server.fetch = mock.Mock(side_effect=lambda x, _: imap_emails[x]) + # we ignore the second arg as the data item/mime-part is + # constant (RFC822) + mocked_imaplib_server.fetch = mock.Mock( + side_effect=lambda x, _: imap_emails[x]) with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: - mocked_imaplib.IMAP4 = mock.Mock(return_value=mocked_imaplib_server) + mocked_imaplib.IMAP4 = mock.Mock( + return_value=mocked_imaplib_server) call_command('get_email') ticket1 = get_object_or_404(Ticket, pk=1) self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) - self.assertEqual(ticket1.title, "example email that crashes django-helpdesk get_email") - self.assertEqual(ticket1.description, """hi, thanks for looking into this :)\n\nhttps://github.com/django-helpdesk/django-helpdesk/issues/567#issuecomment-342954233""") + self.assertEqual( + ticket1.title, "example email that crashes django-helpdesk get_email") + self.assertEqual( + ticket1.description, """hi, thanks for looking into this :)\n\nhttps://github.com/django-helpdesk/django-helpdesk/issues/567#issuecomment-342954233""") # MIME part should be attached to follow up followup1 = get_object_or_404(FollowUp, pk=1) self.assertEqual(followup1.ticket.id, 1) @@ -680,9 +765,11 @@ class GetEmailCCHandling(TestCase): self.assertEqual(len(TicketCC.objects.filter(ticket=1)), 1) ccstaff = get_object_or_404(TicketCC, pk=1) self.assertEqual(ccstaff.user, User.objects.get(username='staff')) - self.assertEqual(ticket1.assigned_to, User.objects.get(username='assigned')) + self.assertEqual(ticket1.assigned_to, + User.objects.get(username='assigned')) - # example email text from Django docs: https://docs.djangoproject.com/en/1.10/ref/unicode/ + # example email text from Django docs: + # https://docs.djangoproject.com/en/1.10/ref/unicode/ test_email_from = "submitter@example.com" # NOTE: CC emails are in alphabetical order and must be tested as such! # implementation uses sets, so only way to ensure tickets created @@ -694,7 +781,10 @@ class GetEmailCCHandling(TestCase): ticket_user_emails = "assigned@example.com, staff@example.com, submitter@example.com, observer@example.com, queue@example.com" test_email_subject = "[CC-1] My visit to Sør-Trøndelag" test_email_body = "Unicode helpdesk comment with an s-hat (ŝ) via email." - test_email = "To: queue@example.com\nCc: " + test_email_cc_one + ", " + test_email_cc_one + ", " + test_email_cc_two + ", " + test_email_cc_three + "\nCC: " + test_email_cc_one + ", " + test_email_cc_three + ", " + test_email_cc_four + ", " + ticket_user_emails + "\nFrom: " + test_email_from + "\nSubject: " + test_email_subject + "\n\n" + test_email_body + test_email = "To: queue@example.com\nCc: " + test_email_cc_one + ", " + test_email_cc_one + ", " + test_email_cc_two + ", " + test_email_cc_three + "\nCC: " + test_email_cc_one + \ + ", " + test_email_cc_three + ", " + test_email_cc_four + ", " + ticket_user_emails + \ + "\nFrom: " + test_email_from + "\nSubject: " + \ + test_email_subject + "\n\n" + test_email_body test_mail_len = len(test_email) with mock.patch('os.listdir') as mocked_listdir, \ @@ -755,5 +845,6 @@ for method, socks in case_matrix: test_name = str( "TestGetEmail%s%s" % (method.capitalize(), socks_str)) - cl = type(test_name, (GetEmailParametricTemplate, TestCase), {"method": method, "socks": socks}) + cl = type(test_name, (GetEmailParametricTemplate, TestCase), + {"method": method, "socks": socks}) setattr(thismodule, test_name, cl) diff --git a/helpdesk/tests/test_kb.py b/helpdesk/tests/test_kb.py index 71bc840d..ed1ab7ba 100644 --- a/helpdesk/tests/test_kb.py +++ b/helpdesk/tests/test_kb.py @@ -4,7 +4,8 @@ from django.test import TestCase from helpdesk.models import KBCategory, KBItem, Queue, Ticket -from helpdesk.tests.helpers import (get_staff_user, reload_urlconf, User, create_ticket, print_response) +from helpdesk.tests.helpers import ( + get_staff_user, reload_urlconf, User, create_ticket, print_response) class KBTests(TestCase): @@ -43,13 +44,16 @@ class KBTests(TestCase): self.assertContains(response, 'This is a test category') def test_kb_category(self): - response = self.client.get(reverse('helpdesk:kb_category', args=("test_cat", ))) + response = self.client.get( + reverse('helpdesk:kb_category', args=("test_cat", ))) self.assertContains(response, 'This is a test category') self.assertContains(response, 'KBItem 1') self.assertContains(response, 'KBItem 2') self.assertContains(response, 'Create New Ticket Queue:') - self.client.login(username=self.user.get_username(), password='password') - response = self.client.get(reverse('helpdesk:kb_category', args=("test_cat", ))) + self.client.login(username=self.user.get_username(), + password='password') + response = self.client.get( + reverse('helpdesk:kb_category', args=("test_cat", ))) self.assertContains(response, '') self.assertContains(response, '0 open tickets') ticket = Ticket.objects.create( @@ -58,23 +62,30 @@ class KBTests(TestCase): kbitem=self.kbitem1, ) ticket.save() - response = self.client.get(reverse('helpdesk:kb_category', args=("test_cat",))) + response = self.client.get( + reverse('helpdesk:kb_category', args=("test_cat",))) self.assertContains(response, '1 open tickets') def test_kb_vote(self): - self.client.login(username=self.user.get_username(), password='password') - response = self.client.get(reverse('helpdesk:kb_vote', args=(self.kbitem1.pk,)) + "?vote=up") - cat_url = reverse('helpdesk:kb_category', args=("test_cat",)) + "?kbitem=1" + self.client.login(username=self.user.get_username(), + password='password') + response = self.client.get( + reverse('helpdesk:kb_vote', args=(self.kbitem1.pk,)) + "?vote=up") + cat_url = reverse('helpdesk:kb_category', + args=("test_cat",)) + "?kbitem=1" self.assertRedirects(response, cat_url) response = self.client.get(cat_url) self.assertContains(response, '1 people found this answer useful of 1') - response = self.client.get(reverse('helpdesk:kb_vote', args=(self.kbitem1.pk,)) + "?vote=down") + response = self.client.get( + reverse('helpdesk:kb_vote', args=(self.kbitem1.pk,)) + "?vote=down") self.assertRedirects(response, cat_url) response = self.client.get(cat_url) self.assertContains(response, '0 people found this answer useful of 1') def test_kb_category_iframe(self): - cat_url = reverse('helpdesk:kb_category', args=("test_cat",)) + "?kbitem=1&submitter_email=foo@bar.cz&title=lol&" + cat_url = reverse('helpdesk:kb_category', args=( + "test_cat",)) + "?kbitem=1&submitter_email=foo@bar.cz&title=lol&" response = self.client.get(cat_url) # Assert that query params are passed on to ticket submit form - self.assertContains(response, "'/tickets/submit/?queue=1&_readonly_fields_=queue&kbitem=1&submitter_email=foo%40bar.cz&title=lol") + self.assertContains( + response, "'/tickets/submit/?queue=1&_readonly_fields_=queue&kbitem=1&submitter_email=foo%40bar.cz&title=lol") diff --git a/helpdesk/tests/test_navigation.py b/helpdesk/tests/test_navigation.py index b84eb9f5..21e51ab9 100644 --- a/helpdesk/tests/test_navigation.py +++ b/helpdesk/tests/test_navigation.py @@ -6,7 +6,8 @@ from django.test import TestCase from helpdesk import settings as helpdesk_settings from helpdesk.models import Queue -from helpdesk.tests.helpers import (get_staff_user, reload_urlconf, User, create_ticket, print_response) +from helpdesk.tests.helpers import ( + get_staff_user, reload_urlconf, User, create_ticket, print_response) from django.test.utils import override_settings @@ -26,13 +27,15 @@ class KBDisabledTestCase(TestCase): """Test proper rendering of navigation.html by accessing the dashboard""" from django.urls import NoReverseMatch - self.client.login(username=get_staff_user().get_username(), password='password') + self.client.login(username=get_staff_user( + ).get_username(), password='password') self.assertRaises(NoReverseMatch, reverse, 'helpdesk:kb_index') try: response = self.client.get(reverse('helpdesk:dashboard')) except NoReverseMatch as e: if 'helpdesk:kb_index' in e.message: - self.fail("Please verify any unchecked references to helpdesk_kb_index (start with navigation.html)") + self.fail( + "Please verify any unchecked references to helpdesk_kb_index (start with navigation.html)") else: raise else: @@ -75,7 +78,8 @@ class NonStaffUsersAllowedTestCase(StaffUserTestCaseMixin, TestCase): """ from helpdesk.decorators import is_helpdesk_staff - user = User.objects.create_user(username='henry.wensleydale', password='gouda', email='wensleydale@example.com') + user = User.objects.create_user( + username='henry.wensleydale', password='gouda', email='wensleydale@example.com') self.assertTrue(is_helpdesk_staff(user)) @@ -91,7 +95,8 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase): def setUp(self): super().setUp() self.non_staff_user_password = "gouda" - self.non_staff_user = User.objects.create_user(username='henry.wensleydale', password=self.non_staff_user_password, email='wensleydale@example.com') + self.non_staff_user = User.objects.create_user( + username='henry.wensleydale', password=self.non_staff_user_password, email='wensleydale@example.com') def test_staff_user_detection(self): """Staff and non-staff users are correctly identified""" @@ -118,7 +123,8 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase): from helpdesk.decorators import is_helpdesk_staff user = self.non_staff_user - self.client.login(username=user.username, password=self.non_staff_user_password) + self.client.login(username=user.username, + password=self.non_staff_user_password) response = self.client.get(reverse('helpdesk:dashboard'), follow=True) self.assertTemplateUsed(response, 'helpdesk/registration/login.html') @@ -128,7 +134,8 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase): """ user = get_staff_user() self.client.login(username=user.username, password="password") - response = self.client.get(reverse('helpdesk:rss_unassigned'), follow=True) + response = self.client.get( + reverse('helpdesk:rss_unassigned'), follow=True) self.assertContains(response, 'Unassigned Open and Reopened tickets') @override_settings(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE=False) @@ -137,21 +144,24 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase): non-staff users should not be able to access rss feeds. """ user = self.non_staff_user - self.client.login(username=user.username, password=self.non_staff_user_password) + self.client.login(username=user.username, + password=self.non_staff_user_password) queue = Queue.objects.create( title="Foo", slug="test_queue", ) rss_urls = [ reverse('helpdesk:rss_user', args=[user.username]), - reverse('helpdesk:rss_user_queue', args=[user.username, 'test_queue']), + reverse('helpdesk:rss_user_queue', args=[ + user.username, 'test_queue']), reverse('helpdesk:rss_queue', args=['test_queue']), reverse('helpdesk:rss_unassigned'), reverse('helpdesk:rss_activity'), ] for rss_url in rss_urls: response = self.client.get(rss_url, follow=True) - self.assertTemplateUsed(response, 'helpdesk/registration/login.html') + self.assertTemplateUsed( + response, 'helpdesk/registration/login.html') class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase): @@ -168,7 +178,8 @@ class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase): """ from helpdesk.decorators import is_helpdesk_staff - user = User.objects.create_user(username='henry.wensleydale', password='gouda', email='wensleydale@example.com') + user = User.objects.create_user( + username='henry.wensleydale', password='gouda', email='wensleydale@example.com') self.assertTrue(is_helpdesk_staff(user)) @@ -179,7 +190,8 @@ class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase): def test_custom_staff_fail(self): from helpdesk.decorators import is_helpdesk_staff - user = User.objects.create_user(username='terry.milton', password='frog', email='milton@example.com') + user = User.objects.create_user( + username='terry.milton', password='frog', email='milton@example.com') self.assertFalse(is_helpdesk_staff(user)) @@ -264,7 +276,8 @@ class ReturnToTicketTestCase(TestCase): def test_non_staff_user(self): from helpdesk.views.staff import return_to_ticket - user = User.objects.create_user(username='henry.wensleydale', password='gouda', email='wensleydale@example.com') + user = User.objects.create_user( + username='henry.wensleydale', password='gouda', email='wensleydale@example.com') ticket = create_ticket() response = return_to_ticket(user, helpdesk_settings, ticket) self.assertEqual(response['location'], ticket.ticket_url) diff --git a/helpdesk/tests/test_per_queue_staff_permission.py b/helpdesk/tests/test_per_queue_staff_permission.py index cb6f1496..c16dd7a2 100644 --- a/helpdesk/tests/test_per_queue_staff_permission.py +++ b/helpdesk/tests/test_per_queue_staff_permission.py @@ -56,11 +56,13 @@ class PerQueueStaffMembershipTestCase(TestCase): for ticket_number in range(1, identifier + 1): Ticket.objects.create( - title='Unassigned Ticket %d in Queue %d' % (ticket_number, identifier), + title='Unassigned Ticket %d in Queue %d' % ( + ticket_number, identifier), queue=queue, ) Ticket.objects.create( - title='Ticket %d in Queue %d Assigned to User_%d' % (ticket_number, identifier, identifier), + title='Ticket %d in Queue %d Assigned to User_%d' % ( + ticket_number, identifier, identifier), queue=queue, assigned_to=user, ) @@ -80,7 +82,8 @@ class PerQueueStaffMembershipTestCase(TestCase): # Regular users for identifier in self.IDENTIFIERS: - self.client.login(username='User_%d' % identifier, password=str(identifier)) + self.client.login(username='User_%d' % + identifier, password=str(identifier)) response = self.client.get(reverse('helpdesk:dashboard')) self.assertEqual( len(response.context['unassigned_tickets']), @@ -117,7 +120,8 @@ class PerQueueStaffMembershipTestCase(TestCase): # Regular users for identifier in self.IDENTIFIERS: - self.client.login(username='User_%d' % identifier, password=str(identifier)) + self.client.login(username='User_%d' % + identifier, password=str(identifier)) response = self.client.get(reverse('helpdesk:report_index')) self.assertEqual( len(response.context['dash_tickets']), @@ -164,9 +168,11 @@ class PerQueueStaffMembershipTestCase(TestCase): """ # Regular users for identifier in self.IDENTIFIERS: - self.client.login(username='User_%d' % identifier, password=str(identifier)) + self.client.login(username='User_%d' % + identifier, password=str(identifier)) response = self.client.get(reverse('helpdesk:list')) - tickets = __Query__(HelpdeskUser(self.identifier_users[identifier]), base64query=response.context['urlsafe_query']).get() + tickets = __Query__(HelpdeskUser( + self.identifier_users[identifier]), base64query=response.context['urlsafe_query']).get() self.assertEqual( len(tickets), identifier * 2, @@ -186,7 +192,8 @@ class PerQueueStaffMembershipTestCase(TestCase): # Superuser self.client.login(username='superuser', password='superuser') response = self.client.get(reverse('helpdesk:list')) - tickets = __Query__(HelpdeskUser(self.superuser), base64query=response.context['urlsafe_query']).get() + tickets = __Query__(HelpdeskUser(self.superuser), + base64query=response.context['urlsafe_query']).get() self.assertEqual( len(tickets), 6, @@ -201,7 +208,8 @@ class PerQueueStaffMembershipTestCase(TestCase): """ # Regular users for identifier in self.IDENTIFIERS: - self.client.login(username='User_%d' % identifier, password=str(identifier)) + self.client.login(username='User_%d' % + identifier, password=str(identifier)) response = self.client.get( reverse('helpdesk:run_report', kwargs={'report': 'userqueue'}) ) @@ -212,9 +220,11 @@ class PerQueueStaffMembershipTestCase(TestCase): 2, 'Queues in report were not properly limited by queue membership' ) - # Each user should see a total number of tickets equal to twice their ID + # Each user should see a total number of tickets equal to twice + # their ID self.assertEqual( - sum([sum(user_tickets[1:]) for user_tickets in response.context['data']]), + sum([sum(user_tickets[1:]) + for user_tickets in response.context['data']]), identifier * 2, 'Tickets in report were not properly limited by queue membership' ) @@ -224,7 +234,8 @@ class PerQueueStaffMembershipTestCase(TestCase): 2, 'Queue choices were not properly limited by queue membership' ) - # The queue each user can pick should be the queue named after their ID + # The queue each user can pick should be the queue named after + # their ID self.assertEqual( response.context['headings'][1], "Queue %d" % identifier, @@ -245,7 +256,8 @@ class PerQueueStaffMembershipTestCase(TestCase): ) # Superuser should see the total ticket count of three tickets self.assertEqual( - sum([sum(user_tickets[1:]) for user_tickets in response.context['data']]), + sum([sum(user_tickets[1:]) + for user_tickets in response.context['data']]), 6, 'Tickets in report were improperly limited by queue membership for a superuser' ) diff --git a/helpdesk/tests/test_public_actions.py b/helpdesk/tests/test_public_actions.py index ffae6d61..feba1a66 100644 --- a/helpdesk/tests/test_public_actions.py +++ b/helpdesk/tests/test_public_actions.py @@ -70,7 +70,8 @@ class PublicActionsTestCase(TestCase): self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html') self.assertEqual(ticket.status, Ticket.CLOSED_STATUS) self.assertEqual(ticket.resolution, resolution_text) - self.assertEqual(current_followups + 1, ticket.followup_set.all().count()) + self.assertEqual(current_followups + 1, + ticket.followup_set.all().count()) ticket.resolution = old_resolution ticket.status = old_status diff --git a/helpdesk/tests/test_query.py b/helpdesk/tests/test_query.py index e861f81f..43ede439 100644 --- a/helpdesk/tests/test_query.py +++ b/helpdesk/tests/test_query.py @@ -5,7 +5,8 @@ from django.urls import reverse from helpdesk.models import KBCategory, KBItem, Queue, Ticket from helpdesk.query import query_to_base64 -from helpdesk.tests.helpers import (get_staff_user, reload_urlconf, User, create_ticket, print_response) +from helpdesk.tests.helpers import ( + get_staff_user, reload_urlconf, User, create_ticket, print_response) class QueryTests(TestCase): @@ -58,7 +59,8 @@ class QueryTests(TestCase): def test_query_basic(self): self.loginUser() query = query_to_base64({}) - response = self.client.get(reverse('helpdesk:datatables_ticket_list', args=[query])) + response = self.client.get( + reverse('helpdesk:datatables_ticket_list', args=[query])) self.assertEqual( response.json(), { @@ -76,12 +78,14 @@ class QueryTests(TestCase): query = query_to_base64( {'filtering': {'kbitem__in': [self.kbitem1.pk]}} ) - response = self.client.get(reverse('helpdesk:datatables_ticket_list', args=[query])) + response = self.client.get( + reverse('helpdesk:datatables_ticket_list', args=[query])) self.assertEqual( response.json(), { "data": - [{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": "now", "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}], + [{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", + "created": "now", "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}], "recordsFiltered": 1, "recordsTotal": 1, "draw": 0, @@ -93,12 +97,14 @@ class QueryTests(TestCase): query = query_to_base64( {'filtering_or': {'kbitem__in': [self.kbitem1.pk]}} ) - response = self.client.get(reverse('helpdesk:datatables_ticket_list', args=[query])) + response = self.client.get( + reverse('helpdesk:datatables_ticket_list', args=[query])) self.assertEqual( response.json(), { "data": - [{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": "now", "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}], + [{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", + "created": "now", "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}], "recordsFiltered": 1, "recordsTotal": 1, "draw": 0, diff --git a/helpdesk/tests/test_ticket_actions.py b/helpdesk/tests/test_ticket_actions.py index b08b3aa9..b0b9daec 100644 --- a/helpdesk/tests/test_ticket_actions.py +++ b/helpdesk/tests/test_ticket_actions.py @@ -78,10 +78,13 @@ class TicketActionsTestCase(TestCase): ticket = Ticket.objects.create(**ticket_data) ticket_id = ticket.id - response = self.client.get(reverse('helpdesk:delete', kwargs={'ticket_id': ticket_id}), follow=True) - self.assertContains(response, 'Are you sure you want to delete this ticket') + response = self.client.get(reverse('helpdesk:delete', kwargs={ + 'ticket_id': ticket_id}), follow=True) + self.assertContains( + response, 'Are you sure you want to delete this ticket') - response = self.client.post(reverse('helpdesk:delete', kwargs={'ticket_id': ticket_id}), follow=True) + response = self.client.post(reverse('helpdesk:delete', kwargs={ + 'ticket_id': ticket_id}), follow=True) first_redirect = response.redirect_chain[0] first_redirect_url = first_redirect[0] @@ -123,7 +126,8 @@ class TicketActionsTestCase(TestCase): post_data = { 'owner': self.user2.id, } - response = self.client.post(reverse('helpdesk:update', kwargs={'ticket_id': ticket_id}), post_data, follow=True) + response = self.client.post(reverse('helpdesk:update', kwargs={ + 'ticket_id': ticket_id}), post_data, follow=True) self.assertContains(response, 'Changed Owner from User_1 to User_2') # change status with users email assigned and submitter email assigned, @@ -142,14 +146,16 @@ class TicketActionsTestCase(TestCase): # do this also to a newly assigned user (different from logged in one) ticket.assigned_to = self.user - response = self.client.post(reverse('helpdesk:update', kwargs={'ticket_id': ticket_id}), post_data, follow=True) + response = self.client.post(reverse('helpdesk:update', kwargs={ + 'ticket_id': ticket_id}), post_data, follow=True) self.assertContains(response, 'Changed Status from Open to Closed') post_data = { 'new_status': Ticket.OPEN_STATUS, 'owner': self.user2.id, 'public': True } - response = self.client.post(reverse('helpdesk:update', kwargs={'ticket_id': ticket_id}), post_data, follow=True) + response = self.client.post(reverse('helpdesk:update', kwargs={ + 'ticket_id': ticket_id}), post_data, follow=True) self.assertContains(response, 'Changed Status from Open to Closed') def test_can_access_ticket(self): @@ -175,8 +181,10 @@ class TicketActionsTestCase(TestCase): # create ticket helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = True ticket = Ticket.objects.create(**initial_data) - self.assertEqual(HelpdeskUser(self.user).can_access_ticket(ticket), True) - self.assertEqual(HelpdeskUser(self.user2).can_access_ticket(ticket), False) + self.assertEqual(HelpdeskUser( + self.user).can_access_ticket(ticket), True) + self.assertEqual(HelpdeskUser( + self.user2).can_access_ticket(ticket), False) def test_num_to_link(self): """Test that we are correctly expanding links to tickets from IDs""" @@ -197,10 +205,13 @@ class TicketActionsTestCase(TestCase): # generate the URL text result = num_to_link('this is ticket#%s' % ticket_id) - self.assertEqual(result, "this is ticket #%s" % (ticket_id, ticket_id)) + self.assertEqual( + result, "this is ticket #%s" % (ticket_id, ticket_id)) - result2 = num_to_link('whoa another ticket is here #%s huh' % ticket_id) - self.assertEqual(result2, "whoa another ticket is here #%s huh" % (ticket_id, ticket_id)) + result2 = num_to_link( + 'whoa another ticket is here #%s huh' % ticket_id) + self.assertEqual( + result2, "whoa another ticket is here #%s huh" % (ticket_id, ticket_id)) def test_create_ticket_getform(self): self.loginUser() @@ -221,7 +232,8 @@ class TicketActionsTestCase(TestCase): status=Ticket.RESOLVED_STATUS, resolution='Awesome resolution for ticket 1' ) - ticket_1_follow_up = ticket_1.followup_set.create(title='Ticket 1 creation') + ticket_1_follow_up = ticket_1.followup_set.create( + title='Ticket 1 creation') ticket_1_cc = ticket_1.ticketcc_set.create(user=self.user) ticket_1_created = ticket_1.created due_date = timezone.now() @@ -233,7 +245,8 @@ class TicketActionsTestCase(TestCase): due_date=due_date, assigned_to=self.user ) - ticket_2_follow_up = ticket_1.followup_set.create(title='Ticket 2 creation') + ticket_2_follow_up = ticket_1.followup_set.create( + title='Ticket 2 creation') ticket_2_cc = ticket_2.ticketcc_set.create(email='random@mail.com') # Create custom fields and set values for tickets @@ -243,16 +256,19 @@ class TicketActionsTestCase(TestCase): data_type='varchar', ) ticket_1_field_1 = 'This is for the test field' - ticket_1.ticketcustomfieldvalue_set.create(field=custom_field_1, value=ticket_1_field_1) + ticket_1.ticketcustomfieldvalue_set.create( + field=custom_field_1, value=ticket_1_field_1) ticket_2_field_1 = 'Another test text' - ticket_2.ticketcustomfieldvalue_set.create(field=custom_field_1, value=ticket_2_field_1) + ticket_2.ticketcustomfieldvalue_set.create( + field=custom_field_1, value=ticket_2_field_1) custom_field_2 = CustomField.objects.create( name='number', label='Number', data_type='integer', ) ticket_2_field_2 = '444' - ticket_2.ticketcustomfieldvalue_set.create(field=custom_field_2, value=ticket_2_field_2) + ticket_2.ticketcustomfieldvalue_set.create( + field=custom_field_2, value=ticket_2_field_2) # Check that it correctly redirects to the intermediate page response = self.client.post( @@ -263,7 +279,8 @@ class TicketActionsTestCase(TestCase): }, follow=True ) - redirect_url = '%s?tickets=%s&tickets=%s' % (reverse('helpdesk:merge_tickets'), ticket_1.id, ticket_2.id) + redirect_url = '%s?tickets=%s&tickets=%s' % ( + reverse('helpdesk:merge_tickets'), ticket_1.id, ticket_2.id) self.assertRedirects(response, redirect_url) self.assertContains(response, ticket_1.description) self.assertContains(response, ticket_1.resolution) @@ -301,7 +318,11 @@ class TicketActionsTestCase(TestCase): self.assertEqual(ticket_1.submitter_email, ticket_2.submitter_email) self.assertEqual(ticket_1.description, ticket_2.description) self.assertEqual(ticket_1.assigned_to, ticket_2.assigned_to) - self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(field=custom_field_1).value, ticket_1_field_1) - self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(field=custom_field_2).value, ticket_2_field_2) - self.assertEqual(list(ticket_1.followup_set.all()), [ticket_1_follow_up, ticket_2_follow_up]) - self.assertEqual(list(ticket_1.ticketcc_set.all()), [ticket_1_cc, ticket_2_cc]) + self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get( + field=custom_field_1).value, ticket_1_field_1) + self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get( + field=custom_field_2).value, ticket_2_field_2) + self.assertEqual(list(ticket_1.followup_set.all()), [ + ticket_1_follow_up, ticket_2_follow_up]) + self.assertEqual(list(ticket_1.ticketcc_set.all()), + [ticket_1_cc, ticket_2_cc]) diff --git a/helpdesk/tests/test_ticket_lookup.py b/helpdesk/tests/test_ticket_lookup.py index 94709425..e2891f95 100644 --- a/helpdesk/tests/test_ticket_lookup.py +++ b/helpdesk/tests/test_ticket_lookup.py @@ -57,7 +57,8 @@ class TestTicketLookupPublicEnabled(TestCase): def test_add_email_to_ticketcc_if_not_in(self): staff_email = 'staff@mail.com' - staff_user = User.objects.create(username='staff', email=staff_email, is_staff=True) + staff_user = User.objects.create( + username='staff', email=staff_email, is_staff=True) self.ticket.assigned_to = staff_user self.ticket.save() email_1 = 'user1@mail.com' @@ -66,20 +67,25 @@ class TestTicketLookupPublicEnabled(TestCase): # Add new email to CC email_2 = 'user2@mail.com' ticketcc_2 = self.ticket.add_email_to_ticketcc_if_not_in(email=email_2) - self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2]) + self.assertEqual(list(self.ticket.ticketcc_set.all()), + [ticketcc_1, ticketcc_2]) # Add existing email, doesn't change anything self.ticket.add_email_to_ticketcc_if_not_in(email=email_1) - self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2]) + self.assertEqual(list(self.ticket.ticketcc_set.all()), + [ticketcc_1, ticketcc_2]) # Add mail from assigned user, doesn't change anything self.ticket.add_email_to_ticketcc_if_not_in(email=staff_email) - self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2]) + self.assertEqual(list(self.ticket.ticketcc_set.all()), + [ticketcc_1, ticketcc_2]) self.ticket.add_email_to_ticketcc_if_not_in(user=staff_user) - self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2]) + self.assertEqual(list(self.ticket.ticketcc_set.all()), + [ticketcc_1, ticketcc_2]) # Move a ticketCC from ticket 1 to ticket 2 - ticket_2 = Ticket.objects.create(queue=self.ticket.queue, title='Ticket 2', submitter_email=email_2) + ticket_2 = Ticket.objects.create( + queue=self.ticket.queue, title='Ticket 2', submitter_email=email_2) self.assertEqual(ticket_2.ticketcc_set.count(), 0) ticket_2.add_email_to_ticketcc_if_not_in(ticketcc=ticketcc_1) self.assertEqual(ticketcc_1.ticket, ticket_2) diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index 54d51efd..90923967 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -51,7 +51,6 @@ class TicketBasicsTestCase(TestCase): self.client = Client() def test_create_ticket_instance_from_payload(self): - """ Ensure that a instance is created whenever an email is sent to a public queue. """ @@ -76,7 +75,8 @@ class TicketBasicsTestCase(TestCase): 'priority': 3, } - response = self.client.post(reverse('helpdesk:home'), post_data, follow=True) + 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] @@ -95,7 +95,6 @@ class TicketBasicsTestCase(TestCase): # Follow up is anonymous self.assertIsNone(ticket.followup_set.first().user) - def test_create_ticket_public_with_hidden_fields(self): email_count = len(mail.outbox) @@ -110,11 +109,11 @@ class TicketBasicsTestCase(TestCase): 'priority': 4, } - response = self.client.post(reverse('helpdesk:home') + "?_hide_fields_=priority", post_data, follow=True) + response = self.client.post( + reverse('helpdesk:home') + "?_hide_fields_=priority", post_data, follow=True) ticket = Ticket.objects.last() self.assertEqual(ticket.priority, 4) - def test_create_ticket_authorized(self): email_count = len(mail.outbox) self.client.force_login(self.user) @@ -130,7 +129,8 @@ class TicketBasicsTestCase(TestCase): 'priority': 3, } - response = self.client.post(reverse('helpdesk:home'), post_data, follow=True) + 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] @@ -188,7 +188,8 @@ class TicketBasicsTestCase(TestCase): 'custom_textfield': 'This is my custom text.', } - response = self.client.post(reverse('helpdesk:home'), post_data, follow=True) + response = self.client.post( + reverse('helpdesk:home'), post_data, follow=True) custom_field_1.delete() last_redirect = response.redirect_chain[-1] @@ -221,7 +222,8 @@ class TicketBasicsTestCase(TestCase): 'priority': 3, } - response = self.client.post(reverse('helpdesk:home'), post_data, follow=True) + 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] @@ -266,7 +268,6 @@ class EmailInteractionsTestCase(TestCase): } def test_create_ticket_from_email_with_message_id(self): - """ Ensure that a instance is created whenever an email is sent to a public queue. Also, make sure that the RFC 2822 field "message-id" is stored on the @@ -302,7 +303,6 @@ class EmailInteractionsTestCase(TestCase): self.assertIn(submitter_email, mail.outbox[0].to) def test_create_ticket_from_email_without_message_id(self): - """ Ensure that a instance is created whenever an email is sent to a public queue. Also, make sure that the RFC 2822 field "message-id" is stored on the @@ -322,7 +322,8 @@ class EmailInteractionsTestCase(TestCase): object_from_message(str(msg), self.queue_public, logger=logger) - ticket = Ticket.objects.get(title=self.ticket_data['title'], queue=self.queue_public, submitter_email=submitter_email) + ticket = Ticket.objects.get( + title=self.ticket_data['title'], queue=self.queue_public, submitter_email=submitter_email) self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id) @@ -417,8 +418,10 @@ class EmailInteractionsTestCase(TestCase): # Ensure that the submitter is notified self.assertIn(submitter_email, mail.outbox[0].to) - # Ensure that the queue's email was not subscribed to the event notifications. - self.assertRaises(TicketCC.DoesNotExist, TicketCC.objects.get, ticket=ticket, email=to_list[0]) + # Ensure that the queue's email was not subscribed to the event + # notifications. + self.assertRaises(TicketCC.DoesNotExist, + TicketCC.objects.get, ticket=ticket, email=to_list[0]) for cc_email in cc_list: @@ -825,14 +828,16 @@ class EmailInteractionsTestCase(TestCase): msg.__setitem__('Message-ID', message_id) msg.__setitem__('Subject', self.ticket_data['title']) msg.__setitem__('From', submitter_email) - msg.__setitem__('To', self.queue_public_with_notifications_disabled.email_address) + msg.__setitem__( + 'To', self.queue_public_with_notifications_disabled.email_address) msg.__setitem__('Cc', ','.join(cc_list)) msg.__setitem__('Content-Type', 'text/plain;') msg.set_payload(self.ticket_data['description']) email_count = len(mail.outbox) - object_from_message(str(msg), self.queue_public_with_notifications_disabled, logger=logger) + object_from_message( + str(msg), self.queue_public_with_notifications_disabled, logger=logger) followup = FollowUp.objects.get(message_id=message_id) ticket = Ticket.objects.get(id=followup.ticket.id) @@ -954,14 +959,16 @@ class EmailInteractionsTestCase(TestCase): msg.__setitem__('Message-ID', message_id) msg.__setitem__('Subject', self.ticket_data['title']) msg.__setitem__('From', submitter_email) - msg.__setitem__('To', self.queue_public_with_notifications_disabled.email_address) + msg.__setitem__( + 'To', self.queue_public_with_notifications_disabled.email_address) msg.__setitem__('Cc', ','.join(cc_list)) msg.__setitem__('Content-Type', 'text/plain;') msg.set_payload(self.ticket_data['description']) email_count = len(mail.outbox) - object_from_message(str(msg), self.queue_public_with_notifications_disabled, logger=logger) + object_from_message( + str(msg), self.queue_public_with_notifications_disabled, logger=logger) followup = FollowUp.objects.get(message_id=message_id) ticket = Ticket.objects.get(id=followup.ticket.id) @@ -994,11 +1001,13 @@ class EmailInteractionsTestCase(TestCase): reply.__setitem__('In-Reply-To', message_id) reply.__setitem__('Subject', self.ticket_data['title']) reply.__setitem__('From', submitter_email) - reply.__setitem__('To', self.queue_public_with_notifications_disabled.email_address) + reply.__setitem__( + 'To', self.queue_public_with_notifications_disabled.email_address) reply.__setitem__('Content-Type', 'text/plain;') reply.set_payload(self.ticket_data['description']) - object_from_message(str(reply), self.queue_public_with_notifications_disabled, logger=logger) + object_from_message( + str(reply), self.queue_public_with_notifications_disabled, logger=logger) followup = FollowUp.objects.get(message_id=message_id) ticket = Ticket.objects.get(id=followup.ticket.id) @@ -1093,8 +1102,12 @@ class EmailInteractionsTestCase(TestCase): answer="A KB Item", ) self.kbitem1.save() - cat_url = reverse('helpdesk:submit') + "?kbitem=1&submitter_email=foo@bar.cz&title=lol" + cat_url = reverse('helpdesk:submit') + \ + "?kbitem=1&submitter_email=foo@bar.cz&title=lol" response = self.client.get(cat_url) - self.assertContains(response, '') - self.assertContains(response, '') - self.assertContains(response, '') + self.assertContains( + response, '') + self.assertContains( + response, '') + self.assertContains( + response, '') diff --git a/helpdesk/tests/test_usersettings.py b/helpdesk/tests/test_usersettings.py index 67ed23f8..293adbf6 100644 --- a/helpdesk/tests/test_usersettings.py +++ b/helpdesk/tests/test_usersettings.py @@ -26,5 +26,6 @@ class TicketActionsTestCase(TestCase): def test_get_user_settings(self): - response = self.client.get(reverse('helpdesk:user_settings'), follow=True) + response = self.client.get( + reverse('helpdesk:user_settings'), follow=True) self.assertContains(response, "Use the following options") diff --git a/helpdesk/urls.py b/helpdesk/urls.py index b87a89a4..9ccd4d38 100644 --- a/helpdesk/urls.py +++ b/helpdesk/urls.py @@ -64,12 +64,16 @@ urlpatterns = [ name="followup_delete", ), path("tickets//edit/", staff.edit_ticket, name="edit"), - path("tickets//update/", staff.update_ticket, name="update"), - path("tickets//delete/", staff.delete_ticket, name="delete"), + path("tickets//update/", + staff.update_ticket, name="update"), + path("tickets//delete/", + staff.delete_ticket, name="delete"), path("tickets//hold/", staff.hold_ticket, name="hold"), - path("tickets//unhold/", staff.unhold_ticket, name="unhold"), + path("tickets//unhold/", + staff.unhold_ticket, name="unhold"), path("tickets//cc/", staff.ticket_cc, name="ticket_cc"), - path("tickets//cc/add/", staff.ticket_cc_add, name="ticket_cc_add"), + path("tickets//cc/add/", + staff.ticket_cc_add, name="ticket_cc_add"), path( "tickets//cc/delete//", staff.ticket_cc_del, @@ -93,13 +97,15 @@ urlpatterns = [ re_path(r"^raw/(?P\w+)/$", staff.raw_details, name="raw"), path("rss/", staff.rss_list, name="rss_index"), path("reports/", staff.report_index, name="report_index"), - re_path(r"^reports/(?P\w+)/$", staff.run_report, name="run_report"), + re_path(r"^reports/(?P\w+)/$", + staff.run_report, name="run_report"), path("save_query/", staff.save_query, name="savequery"), path("delete_query//", staff.delete_saved_query, name="delete_query"), path("settings/", staff.EditUserSettingsView.as_view(), name="user_settings"), path("ignore/", staff.email_ignore, name="email_ignore"), path("ignore/add/", staff.email_ignore_add, name="email_ignore_add"), - path("ignore/delete//", staff.email_ignore_del, name="email_ignore_del"), + path("ignore/delete//", + staff.email_ignore_del, name="email_ignore_del"), re_path( r"^datatables_ticket_list/(?P{})$".format(base64_pattern), staff.datatables_ticket_list, @@ -140,7 +146,8 @@ urlpatterns += [ name="success_iframe", ), path("view/", public.view_ticket, name="public_view"), - path("change_language/", public.change_language, name="public_change_language"), + path("change_language/", public.change_language, + name="public_change_language"), ] urlpatterns += [ @@ -177,7 +184,8 @@ if helpdesk_settings.HELPDESK_ACTIVATE_API_ENDPOINT: router = DefaultRouter() router.register(r"tickets", TicketViewSet, basename="ticket") router.register(r"followups", FollowUpViewSet, basename="followups") - router.register(r"followups-attachments", FollowUpAttachmentViewSet, basename="followupattachments") + router.register(r"followups-attachments", + FollowUpAttachmentViewSet, basename="followupattachments") router.register(r"users", CreateUserView, basename="user") urlpatterns += [re_path(r"^api/", include(router.urls))] @@ -211,7 +219,8 @@ urlpatterns += [ if helpdesk_settings.HELPDESK_KB_ENABLED: urlpatterns += [ path("kb/", kb.index, name="kb_index"), - re_path(r"^kb/(?P[A-Za-z0-9_-]+)/$", kb.category, name="kb_category"), + re_path(r"^kb/(?P[A-Za-z0-9_-]+)/$", + kb.category, name="kb_category"), path("kb//vote/", kb.vote, name="kb_vote"), re_path( r"^kb_iframe/(?P[A-Za-z0-9_-]+)/$", @@ -229,7 +238,8 @@ urlpatterns += [ path( "system_settings/", login_required( - DirectTemplateView.as_view(template_name="helpdesk/system_settings.html") + DirectTemplateView.as_view( + template_name="helpdesk/system_settings.html") ), name="system_settings", ), diff --git a/helpdesk/user.py b/helpdesk/user.py index 152a88e3..eb11d5d0 100644 --- a/helpdesk/user.py +++ b/helpdesk/user.py @@ -11,6 +11,7 @@ if helpdesk_settings.HELPDESK_KB_ENABLED: KBItem ) + def huser_from_request(req): return HelpdeskUser(req.user) @@ -33,7 +34,8 @@ class HelpdeskUser: 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)] + id_list = [q.pk for q in all_queues if user.has_perm( + q.permission_name)] id_list += public_ids return all_queues.filter(pk__in=id_list) else: diff --git a/helpdesk/validators.py b/helpdesk/validators.py index f7e5b5f5..bd2cd522 100644 --- a/helpdesk/validators.py +++ b/helpdesk/validators.py @@ -4,8 +4,11 @@ from django.conf import settings -#TODO: can we use the builtin Django validator instead? -# see: https://docs.djangoproject.com/en/4.0/ref/validators/#fileextensionvalidator +# TODO: can we use the builtin Django validator instead? +# see: +# https://docs.djangoproject.com/en/4.0/ref/validators/#fileextensionvalidator + + def validate_file_extension(value): import os from django.core.exceptions import ValidationError @@ -19,9 +22,12 @@ def validate_file_extension(value): if hasattr(settings, 'VALID_EXTENSIONS'): valid_extensions = settings.VALID_EXTENSIONS else: - valid_extensions = ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml'] + valid_extensions = ['.txt', '.asc', '.htm', '.html', + '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml'] if not ext.lower() in valid_extensions: - # TODO: one more check in case it is a file with no extension; we should always allow that? + # TODO: one more check in case it is a file with no extension; we + # should always allow that? if not (ext.lower() == '' or ext.lower() == '.'): - raise ValidationError('Unsupported file extension: %s.' % ext.lower()) + raise ValidationError( + 'Unsupported file extension: %s.' % ext.lower()) diff --git a/helpdesk/views/abstract_views.py b/helpdesk/views/abstract_views.py index d985dd46..f7e5eb62 100644 --- a/helpdesk/views/abstract_views.py +++ b/helpdesk/views/abstract_views.py @@ -6,15 +6,18 @@ class AbstractCreateTicketMixin(): initial_data = {} request = self.request try: - initial_data['queue'] = Queue.objects.get(slug=request.GET.get('queue', None)).id + initial_data['queue'] = Queue.objects.get( + slug=request.GET.get('queue', None)).id except Queue.DoesNotExist: pass u = request.user if u.is_authenticated and u.usersettings_helpdesk.use_email_as_submitter and u.email: initial_data['submitter_email'] = u.email - query_param_fields = ['submitter_email', 'title', 'body', 'queue', 'kbitem'] - custom_fields = ["custom_%s" % f.name for f in CustomField.objects.filter(staff_only=False)] + query_param_fields = ['submitter_email', + 'title', 'body', 'queue', 'kbitem'] + custom_fields = [ + "custom_%s" % f.name for f in CustomField.objects.filter(staff_only=False)] query_param_fields += custom_fields for qpf in query_param_fields: initial_data[qpf] = request.GET.get(qpf, initial_data.get(qpf, "")) @@ -29,7 +32,8 @@ class AbstractCreateTicketMixin(): ) if kbitem: try: - kwargs['kbcategory'] = KBItem.objects.get(pk=int(kbitem)).category + kwargs['kbcategory'] = KBItem.objects.get( + pk=int(kbitem)).category except (ValueError, KBItem.DoesNotExist): pass return kwargs diff --git a/helpdesk/views/feeds.py b/helpdesk/views/feeds.py index 7ae8ebcb..7975fa4e 100644 --- a/helpdesk/views/feeds.py +++ b/helpdesk/views/feeds.py @@ -123,7 +123,8 @@ class RecentFollowUps(Feed): description_template = 'helpdesk/rss/recent_activity_description.html' title = _('Helpdesk: Recent Followups') - description = _('Recent FollowUps, such as e-mail replies, comments, attachments and resolutions') + description = _( + 'Recent FollowUps, such as e-mail replies, comments, attachments and resolutions') link = '/tickets/' # reverse('helpdesk:list') def items(self): diff --git a/helpdesk/views/public.py b/helpdesk/views/public.py index b0c97c24..7a0b28bf 100644 --- a/helpdesk/views/public.py +++ b/helpdesk/views/public.py @@ -45,7 +45,8 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView): def get_form_class(self): try: - the_module, the_form_class = helpdesk_settings.HELPDESK_PUBLIC_TICKET_FORM_CLASS.rsplit(".", 1) + the_module, the_form_class = helpdesk_settings.HELPDESK_PUBLIC_TICKET_FORM_CLASS.rsplit( + ".", 1) the_module = import_module(the_module) the_form_class = getattr(the_module, the_form_class) except Exception as e: @@ -87,7 +88,8 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView): "Public queue '%s' is configured as default but can't be found", settings.HELPDESK_PUBLIC_TICKET_QUEUE ) - raise ImproperlyConfigured("Wrong public queue configuration") from e + raise ImproperlyConfigured( + "Wrong public queue configuration") from e if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'): initial_data['priority'] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_DUE_DATE'): @@ -97,8 +99,10 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView): def get_form_kwargs(self, *args, **kwargs): kwargs = super().get_form_kwargs(*args, **kwargs) if '_hide_fields_' in self.request.GET: - kwargs['hidden_fields'] = self.request.GET.get('_hide_fields_', '').split(',') - kwargs['readonly_fields'] = self.request.GET.get('_readonly_fields_', '').split(',') + kwargs['hidden_fields'] = self.request.GET.get( + '_hide_fields_', '').split(',') + kwargs['readonly_fields'] = self.request.GET.get( + '_readonly_fields_', '').split(',') return kwargs def form_valid(self, form): @@ -107,7 +111,8 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView): # This submission is spam. Let's not save it. return render(request, template_name='helpdesk/public_spam.html') else: - ticket = form.save(user=self.request.user if self.request.user.is_authenticated else None) + ticket = form.save( + user=self.request.user if self.request.user.is_authenticated else None) try: return HttpResponseRedirect('%s?ticket=%s&email=%s&key=%s' % ( reverse('helpdesk:public_view'), @@ -146,7 +151,8 @@ class CreateTicketView(BaseCreateTicketView): def get_form(self, form_class=None): form = super().get_form(form_class) - # Add the CSS error class to the form in order to better see them in the page + # Add the CSS error class to the form in order to better see them in + # the page form.error_css_class = 'text-danger' return form @@ -156,7 +162,8 @@ class Homepage(CreateTicketView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['kb_categories'] = huser_from_request(self.request).get_allowed_kb_categories() + context['kb_categories'] = huser_from_request( + self.request).get_allowed_kb_categories() return context @@ -170,7 +177,8 @@ def search_for_ticket(request, error_message=None): 'helpdesk_settings': helpdesk_settings, }) else: - raise PermissionDenied("Public viewing of tickets without a secret key is forbidden.") + raise PermissionDenied( + "Public viewing of tickets without a secret key is forbidden.") @protect_view @@ -188,9 +196,11 @@ def view_ticket(request): queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req) try: if hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC: - ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email) + ticket = Ticket.objects.get( + id=ticket_id, submitter_email__iexact=email) else: - ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email, secret_key__iexact=key) + ticket = Ticket.objects.get( + id=ticket_id, submitter_email__iexact=email, secret_key__iexact=key) except (ObjectDoesNotExist, ValueError): return search_for_ticket(request, _('Invalid ticket ID or e-mail address. Please try again.')) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index c4f70f6b..94117637 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -110,7 +110,8 @@ def dashboard(request): # page vars for the three ticket tables user_tickets_page = request.GET.get(_('ut_page'), 1) user_tickets_closed_resolved_page = request.GET.get(_('utcr_page'), 1) - all_tickets_reported_by_current_user_page = request.GET.get(_('atrbcu_page'), 1) + all_tickets_reported_by_current_user_page = request.GET.get( + _('atrbcu_page'), 1) huser = HelpdeskUser(request.user) active_tickets = Ticket.objects.select_related('queue').exclude( @@ -335,7 +336,8 @@ def view_ticket(request, ticket_id): return update_ticket(request, ticket_id) if 'subscribe' in request.GET: - # Allow the user to subscribe him/herself to the ticket whilst viewing it. + # Allow the user to subscribe him/herself to the ticket whilst viewing + # it. ticket_cc, show_subscribe = \ return_ticketccstring_and_show_subscribe(request.user, ticket) if show_subscribe: @@ -361,9 +363,11 @@ def view_ticket(request, ticket_id): return update_ticket(request, ticket_id) if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS: - users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) + users = User.objects.filter( + is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) else: - users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD) + users = User.objects.filter( + is_active=True).order_by(User.USERNAME_FIELD) queues = HelpdeskUser(request.user).get_queues() queue_choices = _get_queue_choices(queues) @@ -378,7 +382,8 @@ def view_ticket(request, ticket_id): if submitter_userprofile is not None: content_type = ContentType.objects.get_for_model(submitter_userprofile) submitter_userprofile_url = reverse( - 'admin:{app}_{model}_change'.format(app=content_type.app_label, model=content_type.model), + 'admin:{app}_{model}_change'.format( + app=content_type.app_label, model=content_type.model), kwargs={'object_id': submitter_userprofile.id} ) else: @@ -439,7 +444,8 @@ def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, ca if ticket is not None: - queryset = TicketCC.objects.filter(ticket=ticket, user=user, email=email) + queryset = TicketCC.objects.filter( + ticket=ticket, user=user, email=email) # Don't create duplicate entries for subscribers if queryset.count() > 0: @@ -509,7 +515,8 @@ def update_ticket(request, ticket_id, public=False): due_date_month = int(request.POST.get('due_date_month', 0)) due_date_day = int(request.POST.get('due_date_day', 0)) if request.POST.get("time_spent"): - (hours, minutes) = [int(f) for f in request.POST.get("time_spent").split(":")] + (hours, minutes) = [int(f) + for f in request.POST.get("time_spent").split(":")] time_spent = timedelta(hours=hours, minutes=minutes) else: time_spent = None @@ -530,12 +537,14 @@ def update_ticket(request, ticket_id, public=False): if not (due_date_year and due_date_month and due_date_day): due_date = ticket.due_date else: - # NOTE: must be an easier way to create a new date than doing it this way? + # NOTE: must be an easier way to create a new date than doing it + # this way? if ticket.due_date: due_date = ticket.due_date else: due_date = timezone.now() - due_date = due_date.replace(due_date_year, due_date_month, due_date_day) + due_date = due_date.replace( + due_date_year, due_date_month, due_date_day) no_changes = all([ not request.FILES, @@ -559,7 +568,8 @@ def update_ticket(request, ticket_id, public=False): # this prevents system from trying to render any template tags # broken into two stages to prevent changes from first replace being themselves # changed by the second replace due to conflicting syntax - comment = comment.replace('{%', 'X-HELPDESK-COMMENT-VERBATIM').replace('%}', 'X-HELPDESK-COMMENT-ENDVERBATIM') + comment = comment.replace( + '{%', 'X-HELPDESK-COMMENT-VERBATIM').replace('%}', 'X-HELPDESK-COMMENT-ENDVERBATIM') comment = comment.replace( 'X-HELPDESK-COMMENT-VERBATIM', '{% verbatim %}{%' ).replace( @@ -699,7 +709,8 @@ def update_ticket(request, ticket_id, public=False): } if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change: roles['assigned_to'] = (template + 'cc', context) - messages_sent_to.update(ticket.send(roles, dont_send_to=messages_sent_to, fail_silently=True, files=files,)) + messages_sent_to.update(ticket.send( + roles, dont_send_to=messages_sent_to, fail_silently=True, files=files,)) if reassigned: template_staff = 'assigned_owner' @@ -741,7 +752,8 @@ def update_ticket(request, ticket_id, public=False): # auto subscribe user if enabled if helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE and request.user.is_authenticated: - ticketcc_string, SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe(request.user, ticket) + ticketcc_string, SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe( + request.user, ticket) if SHOW_SUBSCRIBE: subscribe_staff_member_to_ticket(ticket, request.user) @@ -779,9 +791,11 @@ def mass_update(request): user = request.user action = 'assign' elif action == 'merge': - # Redirect to the Merge View with selected tickets id in the GET request + # Redirect to the Merge View with selected tickets id in the GET + # request return redirect( - reverse('helpdesk:merge_tickets') + '?' + '&'.join(['tickets=%s' % ticket_id for ticket_id in tickets]) + reverse('helpdesk:merge_tickets') + '?' + + '&'.join(['tickets=%s' % ticket_id for ticket_id in tickets]) ) huser = HelpdeskUser(request.user) @@ -871,7 +885,8 @@ def mass_update(request): mass_update = staff_member_required(mass_update) -# Prepare ticket attributes which will be displayed in the table to choose which value to keep when merging +# Prepare ticket attributes which will be displayed in the table to choose +# which value to keep when merging ticket_attributes = ( ('created', _('Created date')), ('due_date', _('Due on')), @@ -914,7 +929,8 @@ def merge_tickets(request): # Prepare the value for each custom fields of this ticket for custom_field in custom_fields: try: - value = ticket.ticketcustomfieldvalue_set.get(field=custom_field).value + value = ticket.ticketcustomfieldvalue_set.get( + field=custom_field).value except (TicketCustomFieldValue.DoesNotExist, ValueError): value = default ticket.values[custom_field.name] = { @@ -925,11 +941,13 @@ def merge_tickets(request): if request.method == 'POST': # Find which ticket has been chosen to be the main one try: - chosen_ticket = tickets.get(id=request.POST.get('chosen_ticket')) + chosen_ticket = tickets.get( + id=request.POST.get('chosen_ticket')) except Ticket.DoesNotExist: ticket_select_form.add_error( field='tickets', - error=_('Please choose a ticket in which the others will be merged into.') + error=_( + 'Please choose a ticket in which the others will be merged into.') ) else: # Save ticket fields values @@ -945,7 +963,8 @@ def merge_tickets(request): if attribute.startswith('get_') and attribute.endswith('_display'): # Keep only the FIELD part attribute = attribute[4:-8] - # Get value from selected ticket and then save it on the chosen ticket + # Get value from selected ticket and then save it on + # the chosen ticket value = getattr(selected_ticket, attribute) setattr(chosen_ticket, attribute, value) # Save custom fields values @@ -953,17 +972,21 @@ def merge_tickets(request): id_for_custom_field = request.POST.get(custom_field.name) if id_for_custom_field != chosen_ticket.id: try: - selected_ticket = tickets.get(id=id_for_custom_field) + selected_ticket = tickets.get( + id=id_for_custom_field) except (Ticket.DoesNotExist, ValueError): continue - # Check if the value for this ticket custom field exists + # Check if the value for this ticket custom field + # exists try: - value = selected_ticket.ticketcustomfieldvalue_set.get(field=custom_field).value + value = selected_ticket.ticketcustomfieldvalue_set.get( + field=custom_field).value except TicketCustomFieldValue.DoesNotExist: continue - # Create the custom field value or update it with the value from the selected ticket + # Create the custom field value or update it with the + # value from the selected ticket custom_field_value, created = chosen_ticket.ticketcustomfieldvalue_set.get_or_create( field=custom_field, defaults={'value': value} @@ -981,31 +1004,39 @@ def merge_tickets(request): ticket.status = Ticket.DUPLICATE_STATUS ticket.save() - # Send mail to submitter email and ticket CC to let them know ticket has been merged + # Send mail to submitter email and ticket CC to let them + # know ticket has been merged context = safe_template_context(ticket) if ticket.submitter_email: send_templated_mail( template_name='merged', context=context, recipients=[ticket.submitter_email], - bcc=[cc.email_address for cc in ticket.ticketcc_set.select_related('user')], + bcc=[ + cc.email_address for cc in ticket.ticketcc_set.select_related('user')], sender=ticket.queue.from_address, fail_silently=True ) - # Move all followups and update their title to know they come from another ticket + # Move all followups and update their title to know they + # come from another ticket ticket.followup_set.update( ticket=chosen_ticket, # Next might exceed maximum 200 characters limit - title=_('[Merged from #%(id)d] %(title)s') % {'id': ticket.id, 'title': ticket.title} + title=_('[Merged from #%(id)d] %(title)s') % { + 'id': ticket.id, 'title': ticket.title} ) - # Add submitter_email, assigned_to email and ticketcc to chosen ticket if necessary - chosen_ticket.add_email_to_ticketcc_if_not_in(email=ticket.submitter_email) + # Add submitter_email, assigned_to email and ticketcc to + # chosen ticket if necessary + chosen_ticket.add_email_to_ticketcc_if_not_in( + email=ticket.submitter_email) if ticket.assigned_to and ticket.assigned_to.email: - chosen_ticket.add_email_to_ticketcc_if_not_in(email=ticket.assigned_to.email) + chosen_ticket.add_email_to_ticketcc_if_not_in( + email=ticket.assigned_to.email) for ticketcc in ticket.ticketcc_set.all(): - chosen_ticket.add_email_to_ticketcc_if_not_in(ticketcc=ticketcc) + chosen_ticket.add_email_to_ticketcc_if_not_in( + ticketcc=ticketcc) return redirect(chosen_ticket) return render(request, 'helpdesk/ticket_merge.html', { @@ -1134,7 +1165,8 @@ def ticket_list(request): urlsafe_query = query_to_base64(query_params) - user_saved_queries = SavedSearch.objects.filter(Q(user=request.user) | Q(shared__exact=True)) + user_saved_queries = SavedSearch.objects.filter( + Q(user=request.user) | Q(shared__exact=True)) search_message = '' if query_params['search_string'] and settings.DATABASES['default']['ENGINE'].endswith('sqlite'): @@ -1150,7 +1182,8 @@ def ticket_list(request): kbitem = [] if helpdesk_settings.HELPDESK_KB_ENABLED: - kbitem_choices = [(item.pk, str(item)) for item in KBItem.objects.all()] + kbitem_choices = [(item.pk, str(item)) + for item in KBItem.objects.all()] kbitem = KBItem.objects.all() return render(request, 'helpdesk/ticket_list.html', dict( @@ -1184,7 +1217,8 @@ def load_saved_query(request, query_params=None): if request.GET.get('saved_query', None): try: saved_query = SavedSearch.objects.get( - Q(pk=request.GET.get('saved_query')) & (Q(shared=True) | Q(user=request.user)) + Q(pk=request.GET.get('saved_query')) & ( + Q(shared=True) | Q(user=request.user)) ) except (SavedSearch.DoesNotExist, ValueError): raise QueryLoadError() @@ -1253,7 +1287,8 @@ class CreateTicketView(MustBeStaffMixin, abstract_views.AbstractCreateTicketMixi return kwargs def form_valid(self, form): - self.ticket = form.save(user=self.request.user if self.request.user.is_authenticated else None) + self.ticket = form.save( + user=self.request.user if self.request.user.is_authenticated else None) return super().form_valid(form) def get_success_url(self): @@ -1580,7 +1615,8 @@ def save_query(request): if not title or not query_encoded: return HttpResponseRedirect(reverse('helpdesk:list')) - query = SavedSearch(title=title, shared=shared, query=query_encoded, user=request.user) + query = SavedSearch(title=title, shared=shared, + query=query_encoded, user=request.user) query.save() return HttpResponseRedirect('%s?saved_query=%s' % (reverse('helpdesk:list'), query.id)) @@ -1679,9 +1715,11 @@ def ticket_cc_add(request, ticket_id): user = form.cleaned_data.get('user') email = form.cleaned_data.get('email') if user and ticket.ticketcc_set.filter(user=user).exists(): - form.add_error('user', _('Impossible to add twice the same user')) + form.add_error( + 'user', _('Impossible to add twice the same user')) elif email and ticket.ticketcc_set.filter(email=email).exists(): - form.add_error('email', _('Impossible to add twice the same email address')) + form.add_error('email', _( + 'Impossible to add twice the same email address')) else: ticketcc = form.save(commit=False) ticketcc.ticket = ticket @@ -1739,7 +1777,8 @@ ticket_dependency_add = staff_member_required(ticket_dependency_add) @helpdesk_staff_member_required def ticket_dependency_del(request, ticket_id, dependency_id): - dependency = get_object_or_404(TicketDependency, ticket__id=ticket_id, 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])) @@ -1798,7 +1837,8 @@ def calc_basic_ticket_stats(Tickets): 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 @@ -1822,7 +1862,8 @@ def calc_basic_ticket_stats(Tickets): 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) + 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) diff --git a/quicktest.py b/quicktest.py index 42a98445..dc576596 100644 --- a/quicktest.py +++ b/quicktest.py @@ -35,8 +35,8 @@ class QuickDjangoTest(object): 'django.contrib.sites', 'django.contrib.staticfiles', 'bootstrap4form', - ## The following commented apps are optional, - ## related to teams functionalities + # The following commented apps are optional, + # related to teams functionalities #'account', #'pinax.invitations', #'pinax.teams', @@ -102,11 +102,11 @@ class QuickDjangoTest(object): TEMPLATES=self.TEMPLATES, SITE_ID=1, SECRET_KEY='wowdonotusethisfakesecuritykeyyouneedarealsecure1', - ## The following settings disable teams - HELPDESK_TEAMS_MODEL = 'auth.User', - HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = [], - HELPDESK_KBITEM_TEAM_GETTER = lambda _: None, - ## test the API + # The following settings disable teams + HELPDESK_TEAMS_MODEL='auth.User', + HELPDESK_TEAMS_MIGRATION_DEPENDENCIES=[], + HELPDESK_KBITEM_TEAM_GETTER=lambda _: None, + # test the API HELPDESK_ACTIVATE_API_ENDPOINT=True ) diff --git a/setup.py b/setup.py index 4ea2cd46..a248621d 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,8 @@ standard_exclude_directories = ( # Note: you may want to copy this into your setup.py file verbatim, as # you can't import this from another package, when you don't know if # that package is installed yet. + + def find_package_data( where=".", package="", @@ -72,7 +74,8 @@ def find_package_data( bad_name = True if show_ignored: print( - "Directory %s ignored by pattern %s" % (fn, pattern), + "Directory %s ignored by pattern %s" % ( + fn, pattern), file=sys.stderr, ) @@ -86,7 +89,8 @@ def find_package_data( new_package = package + "." + name stack.append((fn, "", new_package, False)) else: - stack.append((fn, prefix + name + "/", package, only_in_packages)) + stack.append((fn, prefix + name + "/", + package, only_in_packages)) elif package or not only_in_packages: # is a file bad_name = False @@ -95,7 +99,8 @@ def find_package_data( bad_name = True if show_ignored: print( - "File %s ignored by pattern %s" % (fn, pattern), + "File %s ignored by pattern %s" % ( + fn, pattern), file=sys.stderr, ) break