From f1f3cad2f21ff573ef1f041cfc42ffd4566bd12d Mon Sep 17 00:00:00 2001 From: DavidVadnais Date: Sat, 22 Mar 2025 15:28:43 -1000 Subject: [PATCH 1/6] migrate makefile from flake8 and autopep8 to ruff --- Makefile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index f34a0de5..bf1d87e9 100644 --- a/Makefile +++ b/Makefile @@ -63,15 +63,13 @@ test: #: format - Run the PEP8 formatter. .PHONY: format format: - autopep8 --exit-code --global-config .flake8 helpdesk - isort --line-length=120 --src helpdesk . + ruff format helpdesk #: checkformat - checks formatting against configured format specifications for the project. .PHONY: checkformat checkformat: - flake8 helpdesk --count --show-source --statistics --exit-zero --max-complexity=20 - isort --line-length=120 --src helpdesk . --check + ruff check helpdesk #: documentation - Build documentation (Sphinx, README, ...). From 091670179bdf5a108a792b6d0f95ce550387c065 Mon Sep 17 00:00:00 2001 From: DavidVadnais Date: Sat, 22 Mar 2025 15:29:19 -1000 Subject: [PATCH 2/6] run ruff formatter --- helpdesk/admin.py | 71 +- helpdesk/apps.py | 6 +- helpdesk/decorators.py | 27 +- helpdesk/email.py | 578 ++++--- helpdesk/exceptions.py | 2 + helpdesk/forms.py | 488 +++--- helpdesk/lib.py | 145 +- .../commands/create_escalation_exclusions.py | 63 +- .../commands/create_queue_permissions.py | 30 +- .../commands/create_usersettings.py | 10 +- .../management/commands/escalate_tickets.py | 73 +- helpdesk/management/commands/get_email.py | 30 +- helpdesk/migrations/0001_initial.py | 1207 ++++++++++++--- .../migrations/0002_populate_usersettings.py | 7 +- .../migrations/0003_initial_data_import.py | 14 +- .../0004_add_per_queue_staff_membership.py | 35 +- helpdesk/migrations/0005_queues_no_null.py | 33 +- helpdesk/migrations/0006_email_maxlength.py | 39 +- .../migrations/0007_max_length_by_integer.py | 13 +- .../migrations/0008_extra_for_permissions.py | 15 +- .../0009_migrate_queuemembership.py | 20 +- .../migrations/0010_remove_queuemembership.py | 13 +- .../0011_admin_related_improvements.py | 26 +- .../migrations/0012_queue_default_owner.py | 16 +- .../0013_email_box_local_dir_and_logging.py | 64 +- .../0014_usersettings_related_name.py | 15 +- .../0015_expand_permission_name_size.py | 16 +- .../migrations/0016_alter_model_options.py | 55 +- .../0017_default_owner_on_delete_null.py | 16 +- helpdesk/migrations/0018_ticket_secret_key.py | 102 +- helpdesk/migrations/0019_ticket_secret_key.py | 16 +- .../migrations/0020_depickle_user_settings.py | 67 +- helpdesk/migrations/0021_voting_tracker.py | 106 +- ..._add_submitter_email_id_field_to_ticket.py | 16 +- ...notifications_on_email_events_to_ticket.py | 13 +- helpdesk/migrations/0024_time_spent.py | 11 +- .../migrations/0025_queue_dedicated_time.py | 13 +- .../migrations/0026_kbitem_attachments.py | 62 +- .../migrations/0027_auto_20200107_1221.py | 102 +- helpdesk/migrations/0028_kbitem_team.py | 15 +- helpdesk/migrations/0029_kbcategory_public.py | 11 +- .../migrations/0030_add_kbcategory_name.py | 36 +- .../migrations/0031_auto_20200225_1440.py | 19 +- helpdesk/migrations/0032_kbitem_enabled.py | 11 +- helpdesk/migrations/0033_ticket_merged_to.py | 16 +- .../0034_create_email_template_for_merged.py | 28 +- .../0035_alter_email_on_ticket_change.py | 21 +- .../0036_add_attachment_validator.py | 25 +- .../0037_alter_queue_email_box_type.py | 21 +- ...ecklist_checklisttemplate_checklisttask.py | 104 +- helpdesk/models.py | 1205 ++++++++------- helpdesk/query.py | 183 +-- helpdesk/serializers.py | 111 +- helpdesk/settings.py | 413 +++-- helpdesk/signals.py | 2 +- helpdesk/templated_email.py | 87 +- helpdesk/templatetags/helpdesk_staff.py | 5 +- helpdesk/templatetags/helpdesk_util.py | 25 +- .../templatetags/load_helpdesk_settings.py | 12 +- helpdesk/templatetags/saved_queries.py | 10 +- helpdesk/templatetags/ticket_to_link.py | 16 +- helpdesk/templatetags/user_admin_url.py | 4 +- helpdesk/tests/helpers.py | 28 +- helpdesk/tests/test_api.py | 515 ++++--- helpdesk/tests/test_attachments.py | 167 +- helpdesk/tests/test_checklist.py | 203 +-- helpdesk/tests/test_get_email.py | 1140 ++++++++------ helpdesk/tests/test_kb.py | 54 +- helpdesk/tests/test_login.py | 33 +- helpdesk/tests/test_markdown.py | 4 +- helpdesk/tests/test_navigation.py | 133 +- .../tests/test_per_queue_staff_permission.py | 170 ++- helpdesk/tests/test_public_actions.py | 73 +- helpdesk/tests/test_query.py | 102 +- helpdesk/tests/test_savequery.py | 25 +- helpdesk/tests/test_ticket_actions.py | 287 ++-- helpdesk/tests/test_ticket_lookup.py | 52 +- helpdesk/tests/test_ticket_submission.py | 617 ++++---- helpdesk/tests/test_time_spent.py | 30 +- helpdesk/tests/test_time_spent_auto.py | 292 ++-- helpdesk/tests/test_usersettings.py | 12 +- helpdesk/tests/test_webhooks.py | 291 ++-- helpdesk/tests/urls.py | 4 +- helpdesk/tests/utils.py | 243 ++- helpdesk/update_ticket.py | 237 +-- helpdesk/urls.py | 54 +- helpdesk/user.py | 31 +- helpdesk/validators.py | 7 +- helpdesk/views/abstract_views.py | 28 +- helpdesk/views/api.py | 37 +- helpdesk/views/feeds.py | 113 +- helpdesk/views/kb.py | 46 +- helpdesk/views/login.py | 18 +- helpdesk/views/public.py | 195 ++- helpdesk/views/staff.py | 1350 +++++++++-------- helpdesk/webhooks.py | 22 +- 96 files changed, 7656 insertions(+), 4972 deletions(-) diff --git a/helpdesk/admin.py b/helpdesk/admin.py index 41901cb8..24e9d49f 100644 --- a/helpdesk/admin.py +++ b/helpdesk/admin.py @@ -1,4 +1,3 @@ - from django.contrib import admin from django.utils.translation import gettext_lazy as _ from helpdesk import settings as helpdesk_settings @@ -16,7 +15,7 @@ from helpdesk.models import ( PreSetReply, Queue, Ticket, - TicketChange + TicketChange, ) @@ -26,7 +25,7 @@ if helpdesk_settings.HELPDESK_KB_ENABLED: @admin.register(Queue) class QueueAdmin(admin.ModelAdmin): - list_display = ('title', 'slug', 'email_address', 'locale', 'time_spent') + list_display = ("title", "slug", "email_address", "locale", "time_spent") prepopulated_fields = {"slug": ("title",)} def time_spent(self, q): @@ -44,11 +43,17 @@ class QueueAdmin(admin.ModelAdmin): @admin.register(Ticket) class TicketAdmin(admin.ModelAdmin): - list_display = ('title', 'status', 'assigned_to', 'queue', - 'hidden_submitter_email', 'time_spent') - date_hierarchy = 'created' - list_filter = ('queue', 'assigned_to', 'status') - search_fields = ('id', 'title') + list_display = ( + "title", + "status", + "assigned_to", + "queue", + "hidden_submitter_email", + "time_spent", + ) + date_hierarchy = "created" + list_filter = ("queue", "assigned_to", "status") + search_fields = ("id", "title") def hidden_submitter_email(self, ticket): if ticket.submitter_email: @@ -58,7 +63,8 @@ class TicketAdmin(admin.ModelAdmin): return "%s@%s" % (username, domain) else: return ticket.submitter_email - hidden_submitter_email.short_description = _('Submitter E-Mail') + + hidden_submitter_email.short_description = _("Submitter E-Mail") def time_spent(self, ticket): return ticket.time_spent @@ -82,51 +88,60 @@ class KBIAttachmentInline(admin.StackedInline): @admin.register(FollowUp) class FollowUpAdmin(admin.ModelAdmin): inlines = [TicketChangeInline, FollowUpAttachmentInline] - list_display = ('ticket_get_ticket_for_url', 'title', 'date', 'ticket', - 'user', 'new_status', 'time_spent') - list_filter = ('user', 'date', 'new_status') + list_display = ( + "ticket_get_ticket_for_url", + "title", + "date", + "ticket", + "user", + "new_status", + "time_spent", + ) + list_filter = ("user", "date", "new_status") def ticket_get_ticket_for_url(self, obj): return obj.ticket.ticket_for_url - ticket_get_ticket_for_url.short_description = _('Slug') + + ticket_get_ticket_for_url.short_description = _("Slug") 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') + readonly_fields = ("voted_by", "downvoted_by") - list_display_links = ('title',) + list_display_links = ("title",) if helpdesk_settings.HELPDESK_KB_ENABLED: + @admin.register(KBCategory) class KBCategoryAdmin(admin.ModelAdmin): - list_display = ('name', 'title', 'slug', 'public') + list_display = ("name", "title", "slug", "public") @admin.register(CustomField) class CustomFieldAdmin(admin.ModelAdmin): - list_display = ('name', 'label', 'data_type') + list_display = ("name", "label", "data_type") @admin.register(EmailTemplate) class EmailTemplateAdmin(admin.ModelAdmin): - list_display = ('template_name', 'heading', 'locale') - list_filter = ('locale', ) + list_display = ("template_name", "heading", "locale") + list_filter = ("locale",) @admin.register(IgnoreEmail) class IgnoreEmailAdmin(admin.ModelAdmin): - list_display = ('name', 'queue_list', 'email_address', 'keep_in_mailbox') + list_display = ("name", "queue_list", "email_address", "keep_in_mailbox") @admin.register(ChecklistTemplate) class ChecklistTemplateAdmin(admin.ModelAdmin): - list_display = ('name', 'task_list') - search_fields = ('name', 'task_list') + list_display = ("name", "task_list") + search_fields = ("name", "task_list") class ChecklistTaskInline(admin.TabularInline): @@ -135,10 +150,10 @@ class ChecklistTaskInline(admin.TabularInline): @admin.register(Checklist) class ChecklistAdmin(admin.ModelAdmin): - list_display = ('name', 'ticket') - search_fields = ('name', 'ticket__id', 'ticket__title') - autocomplete_fields = ('ticket',) - list_select_related = ('ticket',) + list_display = ("name", "ticket") + search_fields = ("name", "ticket__id", "ticket__title") + autocomplete_fields = ("ticket",) + list_select_related = ("ticket",) inlines = (ChecklistTaskInline,) diff --git a/helpdesk/apps.py b/helpdesk/apps.py index d316d314..ea98449f 100644 --- a/helpdesk/apps.py +++ b/helpdesk/apps.py @@ -2,12 +2,12 @@ from django.apps import AppConfig class HelpdeskConfig(AppConfig): - name = 'helpdesk' + 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 - default_auto_field = 'django.db.models.AutoField' + default_auto_field = "django.db.models.AutoField" def ready(self): - from . import webhooks # noqa: F401 + from . import webhooks # noqa: F401 diff --git a/helpdesk/decorators.py b/helpdesk/decorators.py index 82ef925d..e3ab87a8 100644 --- a/helpdesk/decorators.py +++ b/helpdesk/decorators.py @@ -12,6 +12,7 @@ def check_staff_status(check_staff=False): The function most only take one User parameter at the end for use with the Django function user_passes_test. """ + def check_superuser_status(check_superuser): def check_user_status(u): is_ok = u.is_authenticated and u.is_active @@ -21,7 +22,9 @@ def check_staff_status(check_staff=False): return is_ok and u.is_superuser else: return is_ok + return check_user_status + return check_superuser_status @@ -43,11 +46,18 @@ def protect_view(view_func): Decorator for protecting the views checking user, redirecting to the log-in page if necessary or returning 404 status code """ + @wraps(view_func) def _wrapped_view(request, *args, **kwargs): - if not request.user.is_authenticated and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT: - return redirect('helpdesk:login') - elif not request.user.is_authenticated and helpdesk_settings.HELPDESK_ANON_ACCESS_RAISES_404: + if ( + not request.user.is_authenticated + and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT + ): + return redirect("helpdesk:login") + elif ( + not request.user.is_authenticated + and helpdesk_settings.HELPDESK_ANON_ACCESS_RAISES_404 + ): raise Http404 if auth_redirect := helpdesk_settings.HELPDESK_PUBLIC_VIEW_PROTECTOR(request): return auth_redirect @@ -61,11 +71,15 @@ def staff_member_required(view_func): Decorator for staff member the views checking user, redirecting to the log-in page if necessary or returning 403 """ + @wraps(view_func) def _wrapped_view(request, *args, **kwargs): if not request.user.is_authenticated and not request.user.is_active: - return redirect('helpdesk:login') - if not helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE and not request.user.is_staff: + return redirect("helpdesk:login") + if ( + not helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE + and not request.user.is_staff + ): raise PermissionDenied() if auth_redirect := helpdesk_settings.HELPDESK_STAFF_VIEW_PROTECTOR(request): return auth_redirect @@ -79,10 +93,11 @@ def superuser_required(view_func): Decorator for superuser member the views checking user, redirecting to the log-in page if necessary or returning 403 """ + @wraps(view_func) def _wrapped_view(request, *args, **kwargs): if not request.user.is_authenticated and not request.user.is_active: - return redirect('helpdesk:login') + return redirect("helpdesk:login") if not request.user.is_superuser: raise PermissionDenied() return view_func(request, *args, **kwargs) diff --git a/helpdesk/email.py b/helpdesk/email.py index 3bb9463c..4256eb7d 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -63,31 +63,36 @@ def process_email(quiet: bool = False, debug_to_stdout: bool = False): print("Extracting email into queues...") q: Queue() # Typing ahead of time for loop to make it more useful in an IDE for q in Queue.objects.filter( - email_box_type__isnull=False, - allow_email_submission=True): + email_box_type__isnull=False, allow_email_submission=True + ): log_msg = f"Processing queue: {q.slug} Email address: {q.email_address}..." if debug_to_stdout: print(log_msg) - logger = logging.getLogger('django.helpdesk.queue.' + q.slug) + logger = logging.getLogger("django.helpdesk.queue." + q.slug) logging_types = { - 'info': logging.INFO, - 'warn': logging.WARN, - 'error': logging.ERROR, - 'crit': logging.CRITICAL, - 'debug': logging.DEBUG, + "info": logging.INFO, + "warn": logging.WARN, + "error": logging.ERROR, + "crit": logging.CRITICAL, + "debug": logging.DEBUG, } if q.logging_type in logging_types: logger.setLevel(logging_types[q.logging_type]) - elif not q.logging_type or q.logging_type == 'none': + elif not q.logging_type or q.logging_type == "none": # disable all handlers so messages go to nowhere logger.handlers = [] logger.propagate = False if quiet: - logger.propagate = False # do not propagate to root logger that would log to console + logger.propagate = ( + False # do not propagate to root logger that would log to console + ) # 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 + 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')) + join(q.logging_dir, q.slug + "_get_email.log") + ) logger.addHandler(log_file_handler) else: log_file_handler = None @@ -107,7 +112,7 @@ def process_email(quiet: bool = False, debug_to_stdout: bool = False): logger.error(f"Queue processing failed: {q.slug} -- {e}", exc_info=True) if debug_to_stdout: print(f"Queue processing failed: {q.slug}") - print("-"*60) + print("-" * 60) traceback.print_exc(file=sys.stdout) finally: # we must close the file handler correctly if it's created @@ -131,7 +136,8 @@ def pop3_sync(q, logger, server): server.stls() except Exception: logger.warning( - "POP3 StartTLS failed or unsupported. Connection will be unencrypted.") + "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) @@ -153,28 +159,32 @@ 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') + full_message = encoding.force_str("\n".join(raw_content), errors="replace") try: - ticket = extract_email_metadata(message=full_message, queue=q, logger=logger) + ticket = extract_email_metadata( + message=full_message, queue=q, logger=logger + ) except IgnoreTicketException: logger.warn( - "Message %s was ignored and will be left on POP3 server" % msgNum) + "Message %s was ignored and will be left on POP3 server" % msgNum + ) except DeleteIgnoredTicketException: - logger.warn( - "Message %s was ignored and deleted from POP3 server" % msgNum) + logger.warn("Message %s was ignored and deleted from POP3 server" % msgNum) server.dele(msgNum) else: if ticket: server.dele(msgNum) logger.info( - "Successfully processed message %s, deleted from POP3 server" % msgNum) + "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) + "Message %s was not successfully processed, and will be left on POP3 server" + % msgNum + ) server.quit() @@ -185,11 +195,12 @@ def imap_sync(q, logger, server): server.starttls() except Exception: 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 - settings.QUEUE_EMAIL_BOX_PASSWORD) + "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 settings.QUEUE_EMAIL_BOX_PASSWORD, + ) server.select(q.email_box_imap_folder) except imaplib.IMAP4.abort: logger.error( @@ -207,36 +218,48 @@ def imap_sync(q, logger, server): sys.exit() try: - data = server.search(None, 'NOT', 'DELETED')[1] + data = server.search(None, "NOT", "DELETED")[1] if data: msgnums = data[0].split() logger.info("Received %d messages from IMAP server" % len(msgnums)) for num in msgnums: logger.info("Processing message %s" % num) - data = server.fetch(num, '(RFC822)')[1] - full_message = encoding.force_str(data[0][1], errors='replace') + data = server.fetch(num, "(RFC822)")[1] + full_message = encoding.force_str(data[0][1], errors="replace") try: - ticket = extract_email_metadata(message=full_message, queue=q, logger=logger) + ticket = extract_email_metadata( + message=full_message, queue=q, logger=logger + ) except IgnoreTicketException: - logger.warn("Message %s was ignored and will be left on IMAP server" % num) + logger.warn( + "Message %s was ignored and will be left on IMAP server" % num + ) except DeleteIgnoredTicketException: - server.store(num, '+FLAGS', '\\Deleted') - logger.warn("Message %s was ignored and deleted from IMAP server" % num) + server.store(num, "+FLAGS", "\\Deleted") + logger.warn( + "Message %s was ignored and deleted from IMAP server" % num + ) except TypeError as te: # Log the error with stacktrace to help identify what went wrong - logger.error(f"Unexpected error processing message: {te}", exc_info=True) + logger.error( + f"Unexpected error processing message: {te}", exc_info=True + ) else: if ticket: - server.store(num, '+FLAGS', '\\Deleted') + server.store(num, "+FLAGS", "\\Deleted") logger.info( - "Successfully processed message %s, deleted from IMAP server" % num) + "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) + "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?", - q.email_box_imap_folder + q.email_box_imap_folder, ) server.expunge() @@ -285,50 +308,61 @@ def imap_oauth_sync(q, logger, server): except ssl.SSLError as e2: logger.error( - f"IMAP login failed due to SSL error. (This is often due to a timeout): {e2}", exc_info=True + f"IMAP login failed due to SSL error. (This is often due to a timeout): {e2}", + exc_info=True, ) server.logout() sys.exit() try: - data = server.search(None, 'NOT', 'DELETED')[1] + data = server.search(None, "NOT", "DELETED")[1] if data: msgnums = data[0].split() logger.info(f"Found {len(msgnums)} message(s) on IMAP server") for num in msgnums: logger.info(f"Processing message {num}") - data = server.fetch(num, '(RFC822)')[1] - full_message = encoding.force_str(data[0][1], errors='replace') + data = server.fetch(num, "(RFC822)")[1] + full_message = encoding.force_str(data[0][1], errors="replace") try: - ticket = extract_email_metadata(message=full_message, queue=q, logger=logger) + ticket = extract_email_metadata( + message=full_message, queue=q, logger=logger + ) except IgnoreTicketException as itex: logger.warn(f"Message {num} was ignored. {itex}") except DeleteIgnoredTicketException: - server.store(num, '+FLAGS', '\\Deleted') + server.store(num, "+FLAGS", "\\Deleted") server.expunge() - logger.warn("Message %s was ignored and deleted from IMAP server" % num) + logger.warn( + "Message %s was ignored and deleted from IMAP server" % num + ) except TypeError as te: # Log the error with stacktrace to help identify what went wrong - logger.error(f"Unexpected error processing message: {te}", exc_info=True) + logger.error( + f"Unexpected error processing message: {te}", exc_info=True + ) else: if ticket: - server.store(num, '+FLAGS', '\\Deleted') + server.store(num, "+FLAGS", "\\Deleted") server.expunge() logger.info( - "Successfully processed message %s, deleted from IMAP server" % num) + "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) + "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?", - q.email_box_imap_folder + q.email_box_imap_folder, ) # Purged Flagged Messages & Logout server.expunge() @@ -337,132 +371,144 @@ def imap_oauth_sync(q, logger, server): def process_queue(q, logger): - logger.info(f"***** {ctime()}: Begin processing mail for django-helpdesk queue: {q.title}") + logger.info( + f"***** {ctime()}: Begin processing mail for django-helpdesk queue: {q.title}" + ) if q.socks_proxy_type and q.socks_proxy_host and q.socks_proxy_port: try: import socks except ImportError: - no_socks_msg = "Queue has been configured with proxy settings, " \ - "but no socks library was installed. Try to " \ - "install PySocks via PyPI." + no_socks_msg = ( + "Queue has been configured with proxy settings, " + "but no socks library was installed. Try to " + "install PySocks via PyPI." + ) logger.error(no_socks_msg) raise ImportError(no_socks_msg) proxy_type = { - 'socks4': socks.SOCKS4, - 'socks5': socks.SOCKS5, + "socks4": socks.SOCKS4, + "socks5": socks.SOCKS5, }.get(q.socks_proxy_type) - socks.set_default_proxy(proxy_type=proxy_type, - addr=q.socks_proxy_host, - port=q.socks_proxy_port) + socks.set_default_proxy( + proxy_type=proxy_type, addr=q.socks_proxy_host, port=q.socks_proxy_port + ) socket.socket = socks.socksocket email_box_type = settings.QUEUE_EMAIL_BOX_TYPE or q.email_box_type mail_defaults = { - 'pop3': { - 'ssl': { - 'port': 995, - 'init': poplib.POP3_SSL, + "pop3": { + "ssl": { + "port": 995, + "init": poplib.POP3_SSL, }, - 'insecure': { - 'port': 110, - 'init': poplib.POP3, + "insecure": { + "port": 110, + "init": poplib.POP3, }, - 'sync': pop3_sync, + "sync": pop3_sync, }, - 'imap': { - 'ssl': { - 'port': 993, - 'init': imaplib.IMAP4_SSL, + "imap": { + "ssl": { + "port": 993, + "init": imaplib.IMAP4_SSL, }, - 'insecure': { - 'port': 143, - 'init': imaplib.IMAP4, + "insecure": { + "port": 143, + "init": imaplib.IMAP4, }, - 'sync': imap_sync + "sync": imap_sync, }, - 'oauth': { - 'ssl': { - 'port': 993, - 'init': imaplib.IMAP4_SSL, + "oauth": { + "ssl": { + "port": 993, + "init": imaplib.IMAP4_SSL, }, - 'insecure': { - 'port': 143, - 'init': imaplib.IMAP4, + "insecure": { + "port": 143, + "init": imaplib.IMAP4, }, - 'sync': imap_oauth_sync + "sync": imap_oauth_sync, }, } if email_box_type in mail_defaults: - encryption = 'insecure' + encryption = "insecure" if q.email_box_ssl or settings.QUEUE_EMAIL_BOX_SSL: - encryption = 'ssl' + encryption = "ssl" if not q.email_box_port: - q.email_box_port = mail_defaults[email_box_type][encryption]['port'] + q.email_box_port = mail_defaults[email_box_type][encryption]["port"] - server = mail_defaults[email_box_type][encryption]['init']( - q.email_box_host or settings.QUEUE_EMAIL_BOX_HOST, - int(q.email_box_port) + server = mail_defaults[email_box_type][encryption]["init"]( + q.email_box_host or settings.QUEUE_EMAIL_BOX_HOST, int(q.email_box_port) ) logger.info("Attempting %s server login" % email_box_type.upper()) - mail_defaults[email_box_type]['sync'](q, logger, server) + mail_defaults[email_box_type]["sync"](q, logger, server) - 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))] + 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)) + ] logger.info("Found %d messages in local mailbox directory" % len(mail)) logger.info("Found %d messages in local mailbox directory" % len(mail)) for i, m in enumerate(mail, 1): logger.info("Processing message %d" % i) - with open(m, 'r') as f: - full_message = encoding.force_str(f.read(), errors='replace') + with open(m, "r") as f: + full_message = encoding.force_str(f.read(), errors="replace") try: - ticket = extract_email_metadata(message=full_message, queue=q, logger=logger) + ticket = extract_email_metadata( + message=full_message, queue=q, logger=logger + ) except IgnoreTicketException: - logger.warn("Message %d was ignored and will be left in local directory", i) + logger.warn( + "Message %d was ignored and will be left in local directory", i + ) except DeleteIgnoredTicketException: os.unlink(m) logger.warn("Message %d was ignored and deleted local directory", i) else: if ticket: logger.info( - "Successfully processed message %d, ticket/comment created.", i) + "Successfully processed message %d, ticket/comment created.", + i, + ) try: # 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) + "Message %d was not successfully processed, and will be left in local directory", + i, + ) def decodeUnknown(charset, string): if string and not isinstance(string, str): if not charset: try: - return str(string, encoding='utf-8', errors='replace') + return str(string, encoding="utf-8", errors="replace") except UnicodeError: - return str(string, encoding='iso8859-1', errors='replace') - return str(string, encoding=charset, errors='replace') + return str(string, encoding="iso8859-1", errors="replace") + return str(string, encoding=charset, errors="replace") return string def decode_mail_headers(string): decoded = email.header.decode_header(string) - return u' '.join([ - str(msg, encoding=charset, errors='replace') if charset else str(msg) - for msg, charset - in decoded - ]) + return " ".join( + [ + str(msg, encoding=charset, errors="replace") if charset else str(msg) + for msg, charset in decoded + ] + ) def is_autoreply(message): @@ -472,10 +518,12 @@ 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"), ] @@ -489,8 +537,8 @@ def create_ticket_cc(ticket, cc_list): new_ticket_ccs = [] from helpdesk.views.staff import subscribe_to_ticket_updates, User - for __, cced_email in cc_list: + for __, cced_email in cc_list: cced_email = cced_email.strip() if cced_email == ticket.queue.email_address: continue @@ -504,7 +552,8 @@ def create_ticket_cc(ticket, cc_list): try: ticket_cc = subscribe_to_ticket_updates( - ticket=ticket, user=user, email=cced_email) + ticket=ticket, user=user, email=cced_email + ) new_ticket_ccs.append(ticket_cc) except ValidationError: pass @@ -516,14 +565,14 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger) ticket, previous_followup, new = None, None, False now = timezone.now() - queue = payload['queue'] - sender_email = payload['sender_email'] + queue = payload["queue"] + sender_email = payload["sender_email"] - to_list = getaddresses(message.get_all('To', [])) - cc_list = getaddresses(message.get_all('Cc', [])) + to_list = getaddresses(message.get_all("To", [])) + cc_list = getaddresses(message.get_all("Cc", [])) - message_id = message.get('Message-Id') - in_reply_to = message.get('In-Reply-To') + message_id = message.get("Message-Id") + in_reply_to = message.get("In-Reply-To") if message_id: message_id = message_id.strip() @@ -533,8 +582,7 @@ 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 @@ -550,24 +598,22 @@ 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 # New issue, create a new instance if ticket is None: if not settings.QUEUE_EMAIL_BOX_UPDATE_ONLY: ticket = Ticket.objects.create( - title=payload['subject'], + title=payload["subject"], queue=queue, submitter_email=sender_email, created=now, - description=payload['body'], - priority=payload['priority'], + description=payload["body"], + 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 # Old issue being re-opened @@ -577,23 +623,33 @@ 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 "", - message_id=message_id + comment=payload.get("full_body", payload["body"]) or "", + message_id=message_id, ) 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") - logger.info("[%s-%s] %s" % (ticket.queue.slug, ticket.id, ticket.title,)) + logger.info( + "[%s-%s] %s" + % ( + ticket.queue.slug, + ticket.id, + ticket.title, + ) + ) if settings.HELPDESK_ENABLE_ATTACHMENTS: try: @@ -604,7 +660,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger) for att_file in attached: logger.info( "Attachment '%s' (with size %s) successfully added to ticket from email.", - att_file[0], att_file[1].size + att_file[0], + att_file[1].size, ) context = safe_template_context(ticket) @@ -615,7 +672,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger) autoreply = is_autoreply(message) if autoreply: logger.info( - "Message seems to be auto-reply, not sending any emails back to the sender") + "Message seems to be auto-reply, not sending any emails back to the sender" + ) else: send_info_email(message_id, f, ticket, context, queue, new) if new: @@ -627,44 +685,48 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger) return ticket -def send_info_email(message_id: str, f: FollowUp, ticket: Ticket, context: dict, queue: dict, new: bool): +def send_info_email( + message_id: str, f: FollowUp, ticket: Ticket, context: dict, queue: dict, new: bool +): # send mail to appropriate people now depending on what objects # were created and who was CC'd # Add auto-reply headers because it's an auto-reply and we must extra_headers = { - 'In-Reply-To': message_id, + "In-Reply-To": message_id, "Auto-Submitted": "auto-replied", "X-Auto-Response-Suppress": "All", "Precedence": "auto_reply", } if new: ticket.send( - {'submitter': ('newticket_submitter', context), - 'new_ticket_cc': ('newticket_cc', context), - 'ticket_cc': ('newticket_cc', context)}, + { + "submitter": ("newticket_submitter", context), + "new_ticket_cc": ("newticket_cc", context), + "ticket_cc": ("newticket_cc", context), + }, fail_silently=True, extra_headers=extra_headers, ) else: context.update(comment=f.comment) ticket.send( - {'submitter': ('updated_submitter', context), - 'assigned_to': ('updated_owner', context)}, + { + "submitter": ("updated_submitter", context), + "assigned_to": ("updated_owner", context), + }, fail_silently=True, extra_headers=extra_headers, ) if queue.enable_notifications_on_email_events: ticket.send( - {'ticket_cc': ('updated_cc', context)}, + {"ticket_cc": ("updated_cc", context)}, fail_silently=True, extra_headers=extra_headers, ) def get_ticket_id_from_subject_slug( - queue_slug: str, - subject: str, - logger: logging.Logger + queue_slug: str, subject: str, logger: logging.Logger ) -> typing.Optional[int]: """Get a ticket id from the subject string @@ -675,37 +737,33 @@ def get_ticket_id_from_subject_slug( ticket_id = None if matchobj: # This is a reply or forward. - ticket_id = matchobj.group('id') + ticket_id = matchobj.group("id") logger.info("Matched tracking ID %s-%s" % (queue_slug, ticket_id)) else: logger.info("No tracking ID matched.") return ticket_id -def add_file_if_always_save_incoming_email_message( - files_, - message: str -) -> None: +def add_file_if_always_save_incoming_email_message(files_, message: str) -> None: """When `settings.HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE` is `True` add a file to the files_ list""" - if getattr(django_settings, 'HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE', False): + if getattr(django_settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False): # save message as attachment in case of some complex markup renders # wrong files_.append( SimpleUploadedFile( _("original_message.eml").replace( - ".eml", - timezone.localtime().strftime("_%d-%m-%Y_%H:%M") + ".eml" + ".eml", timezone.localtime().strftime("_%d-%m-%Y_%H:%M") + ".eml" ), str(message).encode("utf-8"), - 'text/plain' + "text/plain", ) ) def get_encoded_body(body: str) -> str: try: - return body.encode('ascii').decode('unicode_escape') + return body.encode("ascii").decode("unicode_escape") except UnicodeEncodeError: return body @@ -719,18 +777,14 @@ def get_email_body_from_part_payload(part) -> str: """Gets an decoded body from the payload part, if the decode fails, returns without encoding""" try: - return encoding.smart_str( - part.get_payload(decode=True) - ) + return encoding.smart_str(part.get_payload(decode=True)) except UnicodeDecodeError: - return encoding.smart_str( - part.get_payload(decode=False) - ) + return encoding.smart_str(part.get_payload(decode=False)) def attempt_body_extract_from_html(message: str) -> str: mail = BeautifulSoup(str(message), "html.parser") - beautiful_body = mail.find('body') + beautiful_body = mail.find("body") body = None full_body = None if beautiful_body: @@ -744,16 +798,20 @@ def attempt_body_extract_from_html(message: str) -> str: return body, full_body -def mime_content_to_string(part: EmailMessage,) -> str: - ''' +def mime_content_to_string( + part: EmailMessage, +) -> str: + """ Extract the content from the MIME body part :param part: the MIME part to extract the content from - ''' + """ content_bytes = part.get_payload(decode=True) charset = part.get_content_charset() # The default for MIME email is 7bit which requires special decoding to utf-8 so make sure # we handle the decoding correctly - if part['Content-Transfer-Encoding'] in [None, '8bit', '7bit'] and (charset == 'utf-8' or charset is None): + if part["Content-Transfer-Encoding"] in [None, "8bit", "7bit"] and ( + charset == "utf-8" or charset is None + ): charset = "unicode_escape" content = decodeUnknown(charset, content_bytes) return content @@ -769,11 +827,11 @@ def parse_email_content(mime_content: str, is_extract_full_email_msg: bool) -> s def extract_email_message_content( - part: MIMEPart, - files: List, - include_chained_msgs: bool, + part: MIMEPart, + files: List, + include_chained_msgs: bool, ) -> (str, str): - ''' + """ Uses the get_body() method of the email package to extract the email message content. If there is an HTML version of the email message content then it is stored as an attachment. If there is a plain text part then that is used for storing the email content aginst the ticket. @@ -784,7 +842,7 @@ def extract_email_message_content( :param files: any MIME parts to be attached are added to this list :param include_chained_msgs: flag to indicate if the entire email message content including past replies must be extracted - ''' + """ message_part: MIMEPart = part.get_body() parent_part: MIMEPart = part content_type = message_part.get_content_type() @@ -792,7 +850,12 @@ def extract_email_message_content( if "multipart/related" == content_type: # We want the actual message text so try again on the related MIME part parent_part = message_part - message_part = message_part.get_body(preferencelist=["html", "plain",]) + message_part = message_part.get_body( + preferencelist=[ + "html", + "plain", + ] + ) content_type = message_part.get_content_type() mime_content = None formatted_body = None # Retain the original content by using a secondary variable if the HTML needs wrapping @@ -802,16 +865,23 @@ def extract_email_message_content( if "{mime_content}" if "\ - {mime_content if formatted_body is None else formatted_body}" + formatted_body = f'\ + {mime_content if formatted_body is None else formatted_body}' files.append( SimpleUploadedFile( HTML_EMAIL_ATTACHMENT_FILENAME, - (mime_content if formatted_body is None else formatted_body).encode("utf-8"), 'text/html', + (mime_content if formatted_body is None else formatted_body).encode( + "utf-8" + ), + "text/html", ) ) # Try to get a plain part message - plain_message_part = parent_part.get_body(preferencelist=["plain",]) + plain_message_part = parent_part.get_body( + preferencelist=[ + "plain", + ] + ) if plain_message_part: # Replace mime_content with the plain text part content mime_content = mime_content_to_string(plain_message_part) @@ -820,7 +890,8 @@ def extract_email_message_content( else: # Try to constitute the HTML response as plain text mime_content, _x = attempt_body_extract_from_html( - mime_content if formatted_body is None else formatted_body) + mime_content if formatted_body is None else formatted_body + ) else: # Is either text/plain or some random content-type so just decode the part content and store as is mime_content = mime_content_to_string(message_part) @@ -839,10 +910,7 @@ def extract_email_message_content( def process_as_attachment( - part: MIMEPart, - counter: int, - files: List, - logger: logging.Logger + part: MIMEPart, counter: int, files: List, logger: logging.Logger ): name = part.get_filename() if name: @@ -851,30 +919,33 @@ def process_as_attachment( ext = mimetypes.guess_extension(part.get_content_type()) name = f"part-{counter}{ext}" # Extract payload accounting for attached multiparts - payload_bytes = part.as_bytes() if part.is_multipart() else part.get_payload(decode=True) + payload_bytes = ( + part.as_bytes() if part.is_multipart() else part.get_payload(decode=True) + ) files.append(SimpleUploadedFile(name, payload_bytes, mimetypes.guess_type(name)[0])) if logger.isEnabledFor(logging.DEBUG): logger.debug("Processed MIME as attachment: %s", name) return -def extract_email_subject(email_msg: EmailMessage,) -> str: - subject = email_msg.get('subject', _('Comment from e-mail')) - subject = decode_mail_headers( - decodeUnknown(email_msg.get_charset(), subject)) +def extract_email_subject( + email_msg: EmailMessage, +) -> str: + subject = email_msg.get("subject", _("Comment from e-mail")) + subject = decode_mail_headers(decodeUnknown(email_msg.get_charset(), subject)) for affix in STRIPPED_SUBJECT_STRINGS: subject = subject.replace(affix, "") return subject.strip() def extract_attachments( - target_part: MIMEPart, - files: List, - logger: logging.Logger, - counter: int = 1, - content_parts_excluded: bool = False, + target_part: MIMEPart, + files: List, + logger: logging.Logger, + counter: int = 1, + content_parts_excluded: bool = False, ) -> (int, bool): - ''' + """ If the MIME part is a multipart and not identified as "inline" or "attachment" then iterate over the sub parts recursively. Otherwise extract MIME part content and add as an attachment. @@ -888,10 +959,13 @@ def extract_attachments( :param counter: the count of MIME parts added as attachment :param content_parts_excluded: the MIME part(s) that provided the message content have been excluded :returns the count of mime parts added as attachments and a boolean if the content parts have been excluded - ''' + """ content_type = target_part.get_content_type() content_maintype = target_part.get_content_maintype() - if "multipart" == content_maintype and target_part.get_content_disposition() not in ['inline', 'attachment']: + if ( + "multipart" == content_maintype + and target_part.get_content_disposition() not in ["inline", "attachment"] + ): # Cycle through all MIME parts in the email extracting the attachments that were not part of the message body # If this is a "related" multipart then we can use the message part excluder iterator directly if "multipart/related" == content_type: @@ -899,24 +973,30 @@ def extract_attachments( # This should really never happen in a properly constructed email message but... logger.warn( "WARNING! Content type MIME parts have been excluded but a multipart/related has been encountered.\ - There may be missing information in attachments.") + There may be missing information in attachments." + ) else: content_parts_excluded = True # Use the iterator that automatically excludes message content parts for part in target_part.iter_attachments(): counter, content_parts_excluded = extract_attachments( - part, files, logger, counter, content_parts_excluded) + part, files, logger, counter, content_parts_excluded + ) # The iterator must be different depending on whether we have already excluded message content parts else: # Content part might be 1 or 2 parts but will be at same level so track finding at least 1 content_part_detected = False for part in target_part.iter_parts(): - if not content_parts_excluded and part.get_content_type() in ["text/plain", "text/html"]: + if not content_parts_excluded and part.get_content_type() in [ + "text/plain", + "text/html", + ]: content_part_detected = True continue # Recurse into the part to process embedded parts counter, content_parts_excluded = extract_attachments( - part, files, logger, counter, content_parts_excluded) + part, files, logger, counter, content_parts_excluded + ) # If we have found 1 or more content parts then flag that the content parts have been ommitted # to ensure that other text/* parts are attached if content_part_detected: @@ -927,11 +1007,10 @@ def extract_attachments( return (counter, content_parts_excluded) -def extract_email_metadata(message: str, - queue: Queue, - logger: logging.Logger - ) -> Ticket: - ''' +def extract_email_metadata( + message: str, queue: Queue, logger: logging.Logger +) -> Ticket: + """ Extracts the text/plain mime part if there is one as the ticket description and stores the text/html part as an attachment if it is present. If no text/plain part is present then it will try to use the text/html part if @@ -953,15 +1032,17 @@ def extract_email_metadata(message: str, :param message: the raw email message received :param queue: the queue that the message is assigned to :param logger: the logger to be used - ''' + """ # 'message' must be an RFC822 formatted message to correctly parse. # NBot sure why but policy explicitly set to default is required for any messages with attachments in them - message_obj: EmailMessage = email.message_from_string(message, EmailMessage, policy=policy.default) + message_obj: EmailMessage = email.message_from_string( + message, EmailMessage, policy=policy.default + ) subject = extract_email_subject(message_obj) - sender_email = _('Unknown Sender') - sender_hdr = message_obj.get('from') + sender_email = _("Unknown Sender") + sender_hdr = message_obj.get("from") if sender_hdr: # Parse the header which extracts the first email address in the list if more than one # The parseaddr method returns a tuple in the form @@ -971,47 +1052,64 @@ def extract_email_metadata(message: str, for ignore in IgnoreEmail.objects.filter(Q(queues=queue) | Q(queues__isnull=True)): if ignore.test(sender_email): - raise IgnoreTicketException() if ignore.keep_in_mailbox else DeleteIgnoredTicketException() + raise ( + IgnoreTicketException() + if ignore.keep_in_mailbox + else DeleteIgnoredTicketException() + ) ticket_id: typing.Optional[int] = get_ticket_id_from_subject_slug( - queue.slug, - subject, - logger + queue.slug, subject, logger ) files = [] # first message in thread, we save full body to avoid losing forwards and things like that - include_chained_msgs = True if ticket_id is None and getattr( - django_settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False) else False - filtered_body, full_body = extract_email_message_content(message_obj, files, include_chained_msgs) + include_chained_msgs = ( + True + if ticket_id is None + and getattr(django_settings, "HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL", False) + else False + ) + filtered_body, full_body = extract_email_message_content( + message_obj, files, include_chained_msgs + ) # If the base part is not a multipart then it will have already been processed as the vbody content so # no need to process attachments - if "multipart" == message_obj.get_content_maintype() and settings.HELPDESK_ENABLE_ATTACHMENTS: + if ( + "multipart" == message_obj.get_content_maintype() + and settings.HELPDESK_ENABLE_ATTACHMENTS + ): # Find and attach all other parts or part contents as attachments - counter, content_parts_excluded = extract_attachments(message_obj, files, logger) + counter, content_parts_excluded = extract_attachments( + message_obj, files, logger + ) if not content_parts_excluded: # Unexpected situation and may mean there is a hole in the email processing logic logger.warning( "Failed to exclude email content when parsing all MIME parts in the multipart.\ - Verify that there were no text/* parts containing message content.") + Verify that there were no text/* parts containing message content." + ) if logger.isEnabledFor(logging.DEBUG): - logger.debug("Email parsed and %s attachments were found and attached.", counter) + logger.debug( + "Email parsed and %s attachments were found and attached.", counter + ) if settings.HELPDESK_ENABLE_ATTACHMENTS: add_file_if_always_save_incoming_email_message(files, message) - smtp_priority = message_obj.get('priority', '') - smtp_importance = message_obj.get('importance', '') - high_priority_types = {'high', 'important', '1', 'urgent'} - priority = 2 if high_priority_types & { - smtp_priority, smtp_importance} else 3 + smtp_priority = message_obj.get("priority", "") + smtp_importance = message_obj.get("importance", "") + high_priority_types = {"high", "important", "1", "urgent"} + priority = 2 if high_priority_types & {smtp_priority, smtp_importance} else 3 payload = { - 'body': filtered_body, - 'full_body': full_body, - 'subject': subject, - 'queue': queue, - 'sender_email': sender_email, - 'priority': priority, - 'files': files, + "body": filtered_body, + "full_body": full_body, + "subject": subject, + "queue": queue, + "sender_email": sender_email, + "priority": priority, + "files": files, } - return create_object_from_email_message(message_obj, ticket_id, payload, files, logger=logger) + return create_object_from_email_message( + message_obj, ticket_id, payload, files, logger=logger + ) diff --git a/helpdesk/exceptions.py b/helpdesk/exceptions.py index 4dbf59a5..269ba081 100644 --- a/helpdesk/exceptions.py +++ b/helpdesk/exceptions.py @@ -2,6 +2,7 @@ class IgnoreTicketException(Exception): """ Raised when an email message is received from a sender who is marked to be ignored """ + pass @@ -10,4 +11,5 @@ class DeleteIgnoredTicketException(Exception): Raised when an email message is received from a sender who is marked to be ignored and the record is tagged to delete the email from the inbox """ + pass diff --git a/helpdesk/forms.py b/helpdesk/forms.py index b2fbdb25..b1f67c99 100644 --- a/helpdesk/forms.py +++ b/helpdesk/forms.py @@ -27,7 +27,7 @@ from helpdesk.models import ( TicketCC, TicketCustomFieldValue, TicketDependency, - UserSettings + UserSettings, ) from helpdesk.settings import ( CUSTOMFIELD_DATE_FORMAT, @@ -55,67 +55,71 @@ 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': + if field.data_type == "varchar": fieldclass = forms.CharField - instanceargs['max_length'] = field.max_length - elif field.data_type == 'text': + instanceargs["max_length"] = field.max_length + elif field.data_type == "text": fieldclass = forms.CharField - instanceargs['widget'] = forms.Textarea( - attrs={'class': 'form-control'}) - instanceargs['max_length'] = field.max_length - elif field.data_type == 'integer': + 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'}) - elif field.data_type == 'decimal': + 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'}) - elif field.data_type == 'list': + instanceargs["decimal_places"] = field.decimal_places + instanceargs["max_digits"] = field.max_length + 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["choices"] = field.get_choices() + 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 raise NameError("Unrecognized data_type %s" % field.data_type) - self.fields['custom_%s' % field.name] = fieldclass(**instanceargs) + self.fields["custom_%s" % field.name] = fieldclass(**instanceargs) class EditTicketForm(CustomFieldMixin, forms.ModelForm): - class Meta: model = Ticket - exclude = ('created', 'modified', 'status', 'on_hold', - 'resolution', 'last_escalation', 'assigned_to') + 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): """ @@ -124,56 +128,62 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm): super(EditTicketForm, self).__init__(*args, **kwargs) # 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"].disabled = True + 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) + ticket=self.instance, field=field + ) initial_value = current_value.value # Attempt to convert from fixed format string to date/time data # type - if 'datetime' == current_value.field.data_type: + if "datetime" == current_value.field.data_type: initial_value = datetime.strptime( - initial_value, CUSTOMFIELD_DATETIME_FORMAT) - elif 'date' == current_value.field.data_type: + initial_value, CUSTOMFIELD_DATETIME_FORMAT + ) + elif "date" == current_value.field.data_type: initial_value = datetime.strptime( - initial_value, CUSTOMFIELD_DATE_FORMAT) - elif 'time' == current_value.field.data_type: + initial_value, CUSTOMFIELD_DATE_FORMAT + ) + elif "time" == current_value.field.data_type: initial_value = datetime.strptime( - initial_value, CUSTOMFIELD_TIME_FORMAT) + 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 + elif "boolean" == current_value.field.data_type: + initial_value = "True" == initial_value except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError): # ValueError error if parsing fails, using initial_value = current_value.value # TypeError if parsing None type pass instanceargs = { - 'label': field.label, - 'help_text': field.help_text, - 'required': field.required, - 'initial': initial_value, + "label": field.label, + "help_text": field.help_text, + "required": field.required, + "initial": initial_value, } self.customfield_to_field(field, instanceargs) def save(self, *args, **kwargs): - for field, value in self.cleaned_data.items(): - if field.startswith('custom_'): - field_name = field.replace('custom_', '', 1) + if field.startswith("custom_"): + field_name = field.replace("custom_", "", 1) customfield = CustomField.objects.get(name=field_name) try: cfv = TicketCustomFieldValue.objects.get( - ticket=self.instance, field=customfield) + ticket=self.instance, field=customfield + ) except ObjectDoesNotExist: cfv = TicketCustomFieldValue( - ticket=self.instance, field=customfield) + ticket=self.instance, field=customfield + ) cfv.value = convert_value(value) cfv.save() @@ -195,21 +205,24 @@ class EditTicketCustomFieldForm(EditTicketForm): if HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST: fields = list(self.fields) for field in fields: - if field != 'id' and field.replace("custom_", "", 1) not in HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST: + if ( + field != "id" + and field.replace("custom_", "", 1) + not in HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST + ): self.fields.pop(field, None) - - def save(self, *args, **kwargs): + def save(self, *args, **kwargs): # if form is saved in a ticket update, it is passed # a followup instance to trace custom fields changes if "followup" in kwargs: - followup = kwargs.pop('followup', None) + followup = kwargs.pop("followup", None) for field, value in self.cleaned_data.items(): - if field.startswith('custom_'): + if field.startswith("custom_"): if value != self.fields[field].initial: c = followup.ticketchange_set.create( - field=field.replace('custom_', '', 1), + field=field.replace("custom_", "", 1), old_value=self.fields[field].initial, new_value=value, ) @@ -218,20 +231,26 @@ class EditTicketCustomFieldForm(EditTicketForm): class Meta: model = Ticket - fields = ('id', 'merged_to',) + fields = ( + "id", + "merged_to", + ) class EditFollowUpForm(forms.ModelForm): - class Meta: model = FollowUp - exclude = ('date', 'user',) + exclude = ( + "date", + "user", + ) def __init__(self, *args, **kwargs): """Filter not opened tickets here.""" super(EditFollowUpForm, self).__init__(*args, **kwargs) self.fields["ticket"].queryset = Ticket.objects.filter( - status__in=Ticket.OPEN_STATUSES) + status__in=Ticket.OPEN_STATUSES + ) class AbstractTicketForm(CustomFieldMixin, forms.Form): @@ -239,73 +258,81 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form): Contain all the common code and fields between "TicketForm" and "PublicTicketForm". This Form is not intended to be used directly. """ + queue = forms.ChoiceField( - widget=forms.Select(attrs={'class': 'form-control'}), - label=_('Queue'), + widget=forms.Select(attrs={"class": "form-control"}), + label=_("Queue"), required=True, - choices=() + choices=(), ) title = forms.CharField( max_length=100, required=True, - widget=forms.TextInput(attrs={'class': 'form-control'}), - label=_('Summary of the problem'), + widget=forms.TextInput(attrs={"class": "form-control"}), + label=_("Summary of the problem"), ) body = forms.CharField( - widget=forms.Textarea(attrs={'class': 'form-control'}), - label=_('Description of your issue'), + 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( - widget=forms.Select(attrs={'class': 'form-control'}), + widget=forms.Select(attrs={"class": "form-control"}), choices=Ticket.PRIORITY_CHOICES, 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'."), + initial=getattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY", "3"), + label=_("Priority"), + help_text=_("Please select a priority carefully. If unsure, leave it as '3'."), ) due_date = forms.DateTimeField( - widget=forms.TextInput( - attrs={'class': 'form-control', '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"], - label=_('Due on'), + input_formats=[ + CUSTOMFIELD_DATE_FORMAT, + CUSTOMFIELD_DATETIME_FORMAT, + "%d/%m/%Y", + "%m/%d/%Y", + "%d.%m.%Y", + ], + label=_("Due on"), ) if helpdesk_settings.HELPDESK_ENABLE_ATTACHMENTS: attachment = forms.FileField( - widget=forms.FileInput(attrs={'class': 'form-control-file'}), + widget=forms.FileInput(attrs={"class": "form-control-file"}), required=False, - label=_('Attach File'), - help_text=_('You can attach a file to this ticket. ' - 'Only file types such as plain text (.txt), ' - 'a document (.pdf, .docx, or .odt), ' - 'or screenshot (.png or .jpg) may be uploaded.'), - validators=[validate_file_extension] + label=_("Attach File"), + help_text=_( + "You can attach a file to this ticket. " + "Only file types such as plain text (.txt), " + "a document (.pdf, .docx, or .odt), " + "or screenshot (.png or .jpg) may be uploaded." + ), + validators=[validate_file_extension], ) - + 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) if helpdesk_settings.HELPDESK_KB_ENABLED: if kbcategory: - self.fields['kbitem'] = forms.ChoiceField( - widget=forms.Select(attrs={'class': 'form-control'}), + self.fields["kbitem"] = forms.ChoiceField( + 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)], + label=_("Knowledge Base Item"), + 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): @@ -316,38 +343,37 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form): for field in queryset: instanceargs = { - 'label': field.label, - 'help_text': field.help_text, - 'required': field.required, + "label": field.label, + "help_text": field.help_text, + "required": field.required, } self.customfield_to_field(field, instanceargs) def _get_queue(self): # this procedure is re-defined for public submission form - return Queue.objects.get(id=int(self.cleaned_data['queue'])) + return Queue.objects.get(id=int(self.cleaned_data["queue"])) def _create_ticket(self): queue = self._get_queue() kbitem = None - if 'kbitem' in self.cleaned_data: - kbitem = KBItem.objects.get(id=int(self.cleaned_data['kbitem'])) + if "kbitem" in self.cleaned_data: + kbitem = KBItem.objects.get(id=int(self.cleaned_data["kbitem"])) ticket = Ticket( - title=self.cleaned_data['title'], - submitter_email=self.cleaned_data['submitter_email'], + title=self.cleaned_data["title"], + submitter_email=self.cleaned_data["submitter_email"], created=timezone.now(), status=Ticket.OPEN_STATUS, queue=queue, - description=self.cleaned_data['body'], + description=self.cleaned_data["body"], priority=self.cleaned_data.get( - 'priority', - getattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY", "3") + "priority", getattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY", "3") ), due_date=self.cleaned_data.get( - 'due_date', - getattr(settings, "HELPDESK_PUBLIC_TICKET_DUE_DATE", None) - ) or None, + "due_date", getattr(settings, "HELPDESK_PUBLIC_TICKET_DUE_DATE", None) + ) + or None, kbitem=kbitem, ) @@ -357,18 +383,19 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form): ticket.save_custom_field_values(self.cleaned_data) def _create_follow_up(self, ticket, title, user=None): - followup = FollowUp(ticket=ticket, - title=title, - date=timezone.now(), - public=True, - comment=self.cleaned_data['body'], - ) + followup = FollowUp( + ticket=ticket, + title=title, + date=timezone.now(), + public=True, + comment=self.cleaned_data["body"], + ) if user: followup.user = user return followup def _attach_files_to_follow_up(self, followup): - files = self.cleaned_data.get('attachment') + files = self.cleaned_data.get("attachment") if files: files = process_attachments(followup, [files]) return files @@ -376,13 +403,18 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form): @staticmethod def _send_messages(ticket, queue, followup, files, user=None): context = safe_template_context(ticket) - context['comment'] = followup.comment + context["comment"] = followup.comment - roles = {'submitter': ('newticket_submitter', context), - 'new_ticket_cc': ('newticket_cc', context), - 'ticket_cc': ('newticket_cc', context)} - if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign: - roles['assigned_to'] = ('assigned_owner', context) + roles = { + "submitter": ("newticket_submitter", context), + "new_ticket_cc": ("newticket_cc", context), + "ticket_cc": ("newticket_cc", context), + } + if ( + ticket.assigned_to + and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign + ): + roles["assigned_to"] = ("assigned_owner", context) ticket.send( roles, fail_silently=True, @@ -394,26 +426,29 @@ class TicketForm(AbstractTicketForm): """ Ticket Form creation for registered users. """ + submitter_email = forms.EmailField( required=False, - label=_('Submitter E-Mail Address'), - widget=forms.TextInput( - attrs={'class': 'form-control', 'type': 'email'}), - help_text=_('This e-mail address will receive copies of all public ' - 'updates to this ticket.'), + label=_("Submitter E-Mail Address"), + 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." + ), ) assigned_to = forms.ChoiceField( widget=( - forms.Select(attrs={'class': 'form-control'}) + forms.Select(attrs={"class": "form-control"}) if not helpdesk_settings.HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO else forms.HiddenInput() ), required=False, - label=_('Case owner'), - help_text=_('If you select an owner other than yourself, they\'ll be ' - 'e-mailed details of this ticket immediately.'), - - choices=() + label=_("Case owner"), + help_text=_( + "If you select an owner other than yourself, they'll be " + "e-mailed details of this ticket immediately." + ), + choices=(), ) def __init__(self, *args, **kwargs): @@ -424,15 +459,18 @@ class TicketForm(AbstractTicketForm): super().__init__(*args, **kwargs) - self.fields['queue'].choices = queue_choices + 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) + 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): @@ -441,9 +479,9 @@ class TicketForm(AbstractTicketForm): """ ticket, queue = self._create_ticket() - if self.cleaned_data['assigned_to']: + if self.cleaned_data["assigned_to"]: try: - u = User.objects.get(id=self.cleaned_data['assigned_to']) + u = User.objects.get(id=self.cleaned_data["assigned_to"]) ticket.assigned_to = u except User.DoesNotExist: ticket.assigned_to = None @@ -451,12 +489,12 @@ class TicketForm(AbstractTicketForm): self._create_custom_fields(ticket) - if self.cleaned_data['assigned_to']: - title = _('Ticket Opened & Assigned to %(name)s') % { - 'name': ticket.get_assigned_to or _("") + if self.cleaned_data["assigned_to"]: + title = _("Ticket Opened & Assigned to %(name)s") % { + "name": ticket.get_assigned_to or _("") } else: - title = _('Ticket Opened') + title = _("Ticket Opened") followup = self._create_follow_up(ticket, title=title, user=user) followup.save() @@ -468,11 +506,9 @@ class TicketForm(AbstractTicketForm): # emit signal when the TicketForm.save is done new_ticket_done.send(sender="TicketForm", ticket=ticket) - self._send_messages(ticket=ticket, - queue=queue, - followup=followup, - files=files, - user=user) + self._send_messages( + ticket=ticket, queue=queue, followup=followup, files=files, user=user + ) return ticket @@ -480,12 +516,12 @@ 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.'), + label=_("Your E-Mail Address"), + help_text=_("We will e-mail you when your ticket is updated."), ) def __init__(self, hidden_fields=(), readonly_fields=(), *args, **kwargs): @@ -502,14 +538,13 @@ class PublicTicketForm(AbstractTicketForm): self.fields[field].disabled = True field_deletion_table = { - 'queue': 'HELPDESK_PUBLIC_TICKET_QUEUE', - 'priority': 'HELPDESK_PUBLIC_TICKET_PRIORITY', - 'due_date': 'HELPDESK_PUBLIC_TICKET_DUE_DATE', + "queue": "HELPDESK_PUBLIC_TICKET_QUEUE", + "priority": "HELPDESK_PUBLIC_TICKET_PRIORITY", + "due_date": "HELPDESK_PUBLIC_TICKET_DUE_DATE", } 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] @@ -520,12 +555,13 @@ class PublicTicketForm(AbstractTicketForm): "There are no public queues defined - public ticket creation is impossible" ) - if 'queue' in self.fields: - self.fields['queue'].choices = [('', '--------')] + [ - (q.id, q.title) for q in public_queues] + if "queue" in self.fields: + self.fields["queue"].choices = [("", "--------")] + [ + (q.id, q.title) for q in public_queues + ] def _get_queue(self): - if getattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE', None) is not None: + if getattr(settings, "HELPDESK_PUBLIC_TICKET_QUEUE", None) is not None: # force queue to be the pre-defined one # (only for public submissions) public_queue = Queue.objects.filter( @@ -534,12 +570,12 @@ class PublicTicketForm(AbstractTicketForm): if not public_queue: logger.fatal( "Public queue '%s' is configured as default but can't be found", - settings.HELPDESK_PUBLIC_TICKET_QUEUE + settings.HELPDESK_PUBLIC_TICKET_QUEUE, ) return public_queue else: # get the queue user entered - return Queue.objects.get(id=int(self.cleaned_data['queue'])) + return Queue.objects.get(id=int(self.cleaned_data["queue"])) def save(self, user): """ @@ -553,7 +589,8 @@ class PublicTicketForm(AbstractTicketForm): self._create_custom_fields(ticket) followup = self._create_follow_up( - ticket, title=_('Ticket Opened Via Web'), user=user) + ticket, title=_("Ticket Opened Via Web"), user=user + ) followup.save() files = self._attach_files_to_follow_up(followup) @@ -561,161 +598,174 @@ class PublicTicketForm(AbstractTicketForm): # emit signal when the PublicTicketForm.save is done new_ticket_done.send(sender="PublicTicketForm", ticket=ticket) - self._send_messages(ticket=ticket, - queue=queue, - followup=followup, - files=files) + self._send_messages(ticket=ticket, queue=queue, followup=followup, files=files) return ticket class UserSettingsForm(forms.ModelForm): - class Meta: model = UserSettings - exclude = ['user', 'settings_pickled'] + exclude = ["user", "settings_pickled"] class EmailIgnoreForm(forms.ModelForm): - class Meta: model = IgnoreEmail exclude = [] class TicketCCForm(forms.ModelForm): - ''' Adds either an email address or helpdesk user as a CC on a Ticket. Used for processing POST requests. ''' + """Adds either an email address or helpdesk user as a CC on a Ticket. Used for processing POST requests.""" class Meta: model = TicketCC - exclude = ('ticket',) + exclude = ("ticket",) def __init__(self, *args, **kwargs): super(TicketCCForm, self).__init__(*args, **kwargs) if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC: - users = User.objects.filter( - is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) + users = User.objects.filter(is_active=True, is_staff=True).order_by( + User.USERNAME_FIELD + ) else: - users = User.objects.filter( - is_active=True).order_by(User.USERNAME_FIELD) - self.fields['user'].queryset = users + users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD) + self.fields["user"].queryset = users class TicketCCUserForm(forms.ModelForm): - ''' Adds a helpdesk user as a CC on a Ticket ''' + """Adds a helpdesk user as a CC on a Ticket""" def __init__(self, *args, **kwargs): super(TicketCCUserForm, self).__init__(*args, **kwargs) if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC: - users = User.objects.filter( - is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) + users = User.objects.filter(is_active=True, is_staff=True).order_by( + User.USERNAME_FIELD + ) else: - users = User.objects.filter( - is_active=True).order_by(User.USERNAME_FIELD) - self.fields['user'].queryset = users + users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD) + self.fields["user"].queryset = users class Meta: model = TicketCC - exclude = ('ticket', 'email',) + exclude = ( + "ticket", + "email", + ) class TicketCCEmailForm(forms.ModelForm): - ''' Adds an email address as a CC on a Ticket ''' + """Adds an email address as a CC on a Ticket""" def __init__(self, *args, **kwargs): super(TicketCCEmailForm, self).__init__(*args, **kwargs) class Meta: model = TicketCC - exclude = ('ticket', 'user',) + exclude = ( + "ticket", + "user", + ) class TicketDependencyForm(forms.ModelForm): - ''' Adds a different ticket as a dependency for this Ticket ''' + """Adds a different ticket as a dependency for this Ticket""" class Meta: model = TicketDependency - fields = ('depends_on',) + fields = ("depends_on",) def __init__(self, ticket, *args, **kwargs): - super(TicketDependencyForm,self).__init__(*args, **kwargs) + super(TicketDependencyForm, self).__init__(*args, **kwargs) # Only open tickets except myself, existing dependencies and parents - self.fields['depends_on'].queryset = Ticket.objects.filter(status__in=Ticket.OPEN_STATUSES).exclude(id=ticket.id).exclude(depends_on__ticket=ticket).exclude(ticketdependency__depends_on=ticket) + self.fields["depends_on"].queryset = ( + Ticket.objects.filter(status__in=Ticket.OPEN_STATUSES) + .exclude(id=ticket.id) + .exclude(depends_on__ticket=ticket) + .exclude(ticketdependency__depends_on=ticket) + ) + class TicketResolvesForm(forms.ModelForm): - ''' Adds this ticket as a dependency for a different ticket ''' + """Adds this ticket as a dependency for a different ticket""" class Meta: model = TicketDependency - fields = ('ticket',) + fields = ("ticket",) def __init__(self, ticket, *args, **kwargs): - super(TicketResolvesForm,self).__init__(*args, **kwargs) + super(TicketResolvesForm, self).__init__(*args, **kwargs) # Only open tickets except myself, existing dependencies and parents - self.fields['ticket'].queryset = Ticket.objects.exclude(status__in=Ticket.OPEN_STATUSES).exclude(id=ticket.id).exclude(depends_on__ticket=ticket).exclude(ticketdependency__depends_on=ticket) + self.fields["ticket"].queryset = ( + Ticket.objects.exclude(status__in=Ticket.OPEN_STATUSES) + .exclude(id=ticket.id) + .exclude(depends_on__ticket=ticket) + .exclude(ticketdependency__depends_on=ticket) + ) class MultipleTicketSelectForm(forms.Form): tickets = forms.ModelMultipleChoiceField( - label=_('Tickets to merge'), + label=_("Tickets to merge"), queryset=Ticket.objects.filter(merged_to=None), - widget=forms.SelectMultiple(attrs={'class': 'form-control'}) + widget=forms.SelectMultiple(attrs={"class": "form-control"}), ) def clean_tickets(self): - tickets = self.cleaned_data.get('tickets') + tickets = self.cleaned_data.get("tickets") if len(tickets) < 2: - raise ValidationError(_('Please choose at least 2 tickets.')) + 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.')) + _( + "All selected tickets must share the same queue in order to be merged." + ) + ) return tickets class ChecklistTemplateForm(forms.ModelForm): name = forms.CharField( - widget=forms.TextInput(attrs={'class': 'form-control'}), + widget=forms.TextInput(attrs={"class": "form-control"}), required=True, ) task_list = forms.JSONField(widget=forms.HiddenInput()) class Meta: model = ChecklistTemplate - fields = ('name', 'task_list') + fields = ("name", "task_list") def clean_task_list(self): - task_list = self.cleaned_data['task_list'] + task_list = self.cleaned_data["task_list"] return list(map(lambda task: task.strip(), task_list)) class ChecklistForm(forms.ModelForm): name = forms.CharField( - widget=forms.TextInput(attrs={'class': 'form-control'}), + widget=forms.TextInput(attrs={"class": "form-control"}), required=True, ) class Meta: model = Checklist - fields = ('name',) + fields = ("name",) class CreateChecklistForm(ChecklistForm): checklist_template = forms.ModelChoiceField( label=_("Template"), queryset=ChecklistTemplate.objects.all(), - widget=forms.Select(attrs={'class': 'form-control'}), + widget=forms.Select(attrs={"class": "form-control"}), required=False, ) class Meta(ChecklistForm.Meta): - fields = ('checklist_template', 'name') + fields = ("checklist_template", "name") class FormControlDeleteFormSet(forms.BaseInlineFormSet): - deletion_widget = forms.CheckboxInput(attrs={'class': 'form-control'}) + deletion_widget = forms.CheckboxInput(attrs={"class": "form-control"}) diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 83f5b1a8..5f7697e4 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -6,34 +6,52 @@ django-helpdesk - A Django powered ticket tracker for small enterprise. lib.py - Common functions (eg multipart e-mail) """ - from datetime import date, datetime, time from django.conf import settings from django.core.exceptions import ValidationError, ImproperlyConfigured from django.utils.encoding import smart_str -from helpdesk.settings import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT +from helpdesk.settings import ( + CUSTOMFIELD_DATE_FORMAT, + CUSTOMFIELD_DATETIME_FORMAT, + CUSTOMFIELD_TIME_FORMAT, +) import logging import mimetypes -logger = logging.getLogger('helpdesk') +logger = logging.getLogger("helpdesk") def ticket_template_context(ticket): context = {} - for field in ('title', 'created', 'modified', 'submitter_email', - 'status', 'get_status_display', 'on_hold', 'description', - 'resolution', 'priority', 'get_priority_display', - 'last_escalation', 'ticket', 'ticket_for_url', 'merged_to', - 'get_status', 'ticket_url', 'staff_url', '_get_assigned_to' - ): + for field in ( + "title", + "created", + "modified", + "submitter_email", + "status", + "get_status_display", + "on_hold", + "description", + "resolution", + "priority", + "get_priority_display", + "last_escalation", + "ticket", + "ticket_for_url", + "merged_to", + "get_status", + "ticket_url", + "staff_url", + "_get_assigned_to", + ): attr = getattr(ticket, field, None) if callable(attr): - context[field] = '%s' % attr() + context[field] = "%s" % attr() else: context[field] = attr - context['assigned_to'] = context['_get_assigned_to'] + context["assigned_to"] = context["_get_assigned_to"] return context @@ -41,7 +59,7 @@ def ticket_template_context(ticket): def queue_template_context(queue): context = {} - for field in ('title', 'slug', 'email_address', 'from_address', 'locale'): + for field in ("title", "slug", "email_address", "from_address", "locale"): attr = getattr(queue, field, None) if callable(attr): context[field] = attr() @@ -67,10 +85,10 @@ def safe_template_context(ticket): """ context = { - 'queue': queue_template_context(ticket.queue), - 'ticket': ticket_template_context(ticket), + "queue": queue_template_context(ticket.queue), + "ticket": ticket_template_context(ticket), } - context['ticket']['queue'] = context['queue'] + context["ticket"]["queue"] = context["queue"] return context @@ -87,41 +105,42 @@ def text_is_spam(text, request): return False from django.contrib.sites.models import Site from django.core.exceptions import ImproperlyConfigured + try: site = Site.objects.get_current() except ImproperlyConfigured: - site = Site(domain='configure-django-sites.com') + site = Site(domain="configure-django-sites.com") # see https://akismet.readthedocs.io/en/latest/overview.html#using-akismet apikey = None - if hasattr(settings, 'TYPEPAD_ANTISPAM_API_KEY'): + if hasattr(settings, "TYPEPAD_ANTISPAM_API_KEY"): apikey = settings.TYPEPAD_ANTISPAM_API_KEY - elif hasattr(settings, 'PYTHON_AKISMET_API_KEY'): + elif hasattr(settings, "PYTHON_AKISMET_API_KEY"): # new env var expected by python-akismet package apikey = settings.PYTHON_AKISMET_API_KEY - elif hasattr(settings, 'AKISMET_API_KEY'): + elif hasattr(settings, "AKISMET_API_KEY"): # deprecated, but kept for backward compatibility apikey = settings.AKISMET_API_KEY else: return False ak = Akismet( - blog_url='http://%s/' % site.domain, + blog_url="http://%s/" % site.domain, key=apikey, ) - if hasattr(settings, 'TYPEPAD_ANTISPAM_API_KEY'): - ak.baseurl = 'api.antispam.typepad.com/1.1/' + if hasattr(settings, "TYPEPAD_ANTISPAM_API_KEY"): + ak.baseurl = "api.antispam.typepad.com/1.1/" if ak.verify_key(): ak_data = { - 'user_ip': request.META.get('REMOTE_ADDR', '127.0.0.1'), - 'user_agent': request.headers.get('User-Agent', ''), - 'referrer': request.headers.get('Referer', ''), - 'comment_type': 'comment', - 'comment_author': '', + "user_ip": request.META.get("REMOTE_ADDR", "127.0.0.1"), + "user_agent": request.headers.get("User-Agent", ""), + "referrer": request.headers.get("Referer", ""), + "comment_type": "comment", + "comment_author": "", } return ak.comment_check(smart_str(text), data=ak_data) @@ -131,12 +150,12 @@ def text_is_spam(text, request): def process_attachments(followup, attached_files): max_email_attachment_size = getattr( - settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000) + settings, "HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE", 512000 + ) attachments = [] errors = set() for attached in attached_files: - if attached.size: from helpdesk.models import FollowUpAttachment @@ -145,9 +164,9 @@ def process_attachments(followup, attached_files): followup=followup, file=attached, filename=filename, - mime_type=attached.content_type or - mimetypes.guess_type(filename, strict=False)[0] or - 'application/octet-stream', + mime_type=attached.content_type + or mimetypes.guess_type(filename, strict=False)[0] + or "application/octet-stream", size=attached.size, ) try: @@ -176,7 +195,7 @@ def format_time_spent(time_spent): if time_spent: time_spent = "{0:02d}h:{1:02d}m".format( int(time_spent.total_seconds()) // 3600, - int(time_spent.total_seconds()) % 3600 // 60 + int(time_spent.total_seconds()) % 3600 // 60, ) else: time_spent = "" @@ -184,7 +203,7 @@ def format_time_spent(time_spent): def convert_value(value): - """ Convert date/time data type to known fixed format string """ + """Convert date/time data type to known fixed format string""" if type(value) is datetime: return value.strftime(CUSTOMFIELD_DATETIME_FORMAT) elif type(value) is date: @@ -201,39 +220,65 @@ def daily_time_spent_calculation(earliest, latest, open_hours): time_spent_seconds = 0 # avoid rendering day in different locale - weekday = ('monday', 'tuesday', 'wednesday', 'thursday', - 'friday', 'saturday', 'sunday')[earliest.weekday()] - + weekday = ( + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + )[earliest.weekday()] + # enforce correct settings MIDNIGHT = 23.9999 start, end = open_hours.get(weekday, (0, MIDNIGHT)) if not 0 <= start <= end <= MIDNIGHT: - raise ImproperlyConfigured("HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS" - f" setting for {weekday} out of (0, 23.9999) boundary") - + raise ImproperlyConfigured( + "HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS" + f" setting for {weekday} out of (0, 23.9999) boundary" + ) + # transform decimals to minutes and seconds - start_hour, start_minute, start_second = int(start), int(start % 1 * 60), int(start * 60 % 1 * 60) - end_hour, end_minute, end_second = int(end), int(end % 1 * 60), int(end * 60 % 1 * 60) + start_hour, start_minute, start_second = ( + int(start), + int(start % 1 * 60), + int(start * 60 % 1 * 60), + ) + end_hour, end_minute, end_second = ( + int(end), + int(end % 1 * 60), + int(end * 60 % 1 * 60), + ) # translate time for delta calculation earliest_f = earliest.hour + earliest.minute / 60 + earliest.second / 3600 - latest_f = latest.hour + latest.minute / 60 + latest.second / (60 * 60) + latest.microsecond / (60 * 60 * 999999) + latest_f = ( + latest.hour + + latest.minute / 60 + + latest.second / (60 * 60) + + latest.microsecond / (60 * 60 * 999999) + ) # if latest time is midnight and close hour is midnight, add a second to the time spent if latest_f >= MIDNIGHT and end == MIDNIGHT: time_spent_seconds += 1 - + if earliest_f < start: - earliest = earliest.replace(hour=start_hour, minute=start_minute, second=start_second) + earliest = earliest.replace( + hour=start_hour, minute=start_minute, second=start_second + ) elif earliest_f >= end: earliest = earliest.replace(hour=end_hour, minute=end_minute, second=end_second) - + if latest_f < start: - latest = latest.replace(hour=start_hour, minute=start_minute, second=start_second) + latest = latest.replace( + hour=start_hour, minute=start_minute, second=start_second + ) elif latest_f >= end: latest = latest.replace(hour=end_hour, minute=end_minute, second=end_second) - + day_delta = latest - earliest time_spent_seconds += day_delta.seconds - - return time_spent_seconds \ No newline at end of file + + return time_spent_seconds diff --git a/helpdesk/management/commands/create_escalation_exclusions.py b/helpdesk/management/commands/create_escalation_exclusions.py index 0d217ccd..6853d33c 100644 --- a/helpdesk/management/commands/create_escalation_exclusions.py +++ b/helpdesk/management/commands/create_escalation_exclusions.py @@ -14,56 +14,56 @@ from django.core.management.base import BaseCommand, CommandError from helpdesk.models import EscalationExclusion, Queue day_names = { - 'monday': 0, - 'tuesday': 1, - 'wednesday': 2, - 'thursday': 3, - 'friday': 4, - 'saturday': 5, - 'sunday': 6, + "monday": 0, + "tuesday": 1, + "wednesday": 2, + "thursday": 3, + "friday": 4, + "saturday": 5, + "sunday": 6, } class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( - '-d', - '--days', - nargs='*', + "-d", + "--days", + nargs="*", choices=list(day_names.keys()), required=True, - help='Days of week (monday, tuesday, etc). Enter the days as space separated list.' + help="Days of week (monday, tuesday, etc). Enter the days as space separated list.", ) parser.add_argument( - '-o', - '--occurrences', + "-o", + "--occurrences", default=1, type=int, - help='Occurrences: How many weeks ahead to exclude this day' + help="Occurrences: How many weeks ahead to exclude this day", ) parser.add_argument( - '-q', - '--queues', - nargs='*', - choices=list(Queue.objects.values_list('slug', flat=True)), - help='Queues to include (default: all). Enter the queues slug as space separated list.' + "-q", + "--queues", + nargs="*", + choices=list(Queue.objects.values_list("slug", flat=True)), + help="Queues to include (default: all). Enter the queues slug as space separated list.", ) parser.add_argument( - '-x', - '--exclude-verbosely', - action='store_true', + "-x", + "--exclude-verbosely", + action="store_true", default=False, - help='Display a list of dates excluded' + help="Display a list of dates excluded", ) def handle(self, *args, **options): - days = options['days'] - occurrences = options['occurrences'] - verbose = options['exclude_verbosely'] - queue_slugs = options['queues'] + days = options["days"] + occurrences = options["occurrences"] + verbose = options["exclude_verbosely"] + queue_slugs = options["queues"] if not (days and occurrences): - raise CommandError('One or more occurrences must be specified.') + raise CommandError("One or more occurrences must be specified.") queues = [] if queue_slugs is not None: @@ -77,12 +77,13 @@ class Command(BaseCommand): if day == workdate.weekday(): if EscalationExclusion.objects.filter(date=workdate).count() == 0: esc = EscalationExclusion.objects.create( - name=f'Auto Exclusion for {day_name}', - date=workdate + name=f"Auto Exclusion for {day_name}", date=workdate ) if verbose: - self.stdout.write(f"Created exclusion for {day_name} {workdate}") + self.stdout.write( + f"Created exclusion for {day_name} {workdate}" + ) for q in queues: esc.queues.add(q) diff --git a/helpdesk/management/commands/create_queue_permissions.py b/helpdesk/management/commands/create_queue_permissions.py index b4d2bb0b..5d64fcd0 100644 --- a/helpdesk/management/commands/create_queue_permissions.py +++ b/helpdesk/management/commands/create_queue_permissions.py @@ -22,25 +22,24 @@ from helpdesk.models import Queue class Command(BaseCommand): - def add_arguments(self, parser): parser.add_argument( - '-q', - '--queues', - nargs='*', - choices=list(Queue.objects.values_list('slug', flat=True)), - help='Queues to include (default: all). Enter the queues slug as space separated list.' + "-q", + "--queues", + nargs="*", + choices=list(Queue.objects.values_list("slug", flat=True)), + help="Queues to include (default: all). Enter the queues slug as space separated list.", ) parser.add_argument( - '-x', - '--escalate-verbosely', - action='store_true', + "-x", + "--escalate-verbosely", + action="store_true", default=False, - help='Display a list of dates excluded' + help="Display a list of dates excluded", ) def handle(self, *args, **options): - queue_slugs = options['queues'] + queue_slugs = options["queues"] if queue_slugs is not None: queues = Queue.objects.filter(slug__in=queue_slugs) @@ -53,16 +52,17 @@ class Command(BaseCommand): if q.permission_name: self.stdout.write( - f" .. already has `permission_name={q.permission_name}`") + f" .. already has `permission_name={q.permission_name}`" + ) basename = q.permission_name[9:] else: basename = q.generate_permission_name() self.stdout.write( - f" .. generated `permission_name={q.permission_name}`") + f" .. generated `permission_name={q.permission_name}`" + ) q.save() - self.stdout.write( - f" .. checking permission codename `{basename}`") + self.stdout.write(f" .. checking permission codename `{basename}`") try: Permission.objects.create( diff --git a/helpdesk/management/commands/create_usersettings.py b/helpdesk/management/commands/create_usersettings.py index 7f5203cf..238b126d 100644 --- a/helpdesk/management/commands/create_usersettings.py +++ b/helpdesk/management/commands/create_usersettings.py @@ -20,10 +20,12 @@ User = get_user_model() class Command(BaseCommand): """create_usersettings command""" - help = _('Check for user without django-helpdesk UserSettings ' - 'and create settings if required. Uses ' - 'settings.DEFAULT_USER_SETTINGS which can be overridden to ' - 'suit your situation.') + help = _( + "Check for user without django-helpdesk UserSettings " + "and create settings if required. Uses " + "settings.DEFAULT_USER_SETTINGS which can be overridden to " + "suit your situation." + ) def handle(self, *args, **options): """handle command line""" diff --git a/helpdesk/management/commands/escalate_tickets.py b/helpdesk/management/commands/escalate_tickets.py index cc9e5458..3857ee74 100644 --- a/helpdesk/management/commands/escalate_tickets.py +++ b/helpdesk/management/commands/escalate_tickets.py @@ -20,34 +20,36 @@ from helpdesk.models import EscalationExclusion, Queue, Ticket class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( - '-q', - '--queues', - nargs='*', - choices=list(Queue.objects.values_list('slug', flat=True)), - help='Queues to include (default: all). Enter the queues slug as space separated list.' + "-q", + "--queues", + nargs="*", + choices=list(Queue.objects.values_list("slug", flat=True)), + help="Queues to include (default: all). Enter the queues slug as space separated list.", ) parser.add_argument( - '-x', - '--escalate-verbosely', - action='store_true', + "-x", + "--escalate-verbosely", + action="store_true", default=False, - help='Display escalated tickets' + help="Display escalated tickets", ) parser.add_argument( - '-n', - '--notify-only', - action='store_true', + "-n", + "--notify-only", + action="store_true", default=False, - help='Send email reminder but dont escalate tickets' + help="Send email reminder but dont escalate tickets", ) def handle(self, *args, **options): - verbose = options['escalate_verbosely'] - notify_only = options['notify_only'] + verbose = options["escalate_verbosely"] + notify_only = options["notify_only"] - queue_slugs = options['queues'] + queue_slugs = options["queues"] # Only include queues with escalation configured - queues = Queue.objects.filter(escalate_days__isnull=False).exclude(escalate_days=0) + queues = Queue.objects.filter(escalate_days__isnull=False).exclude( + escalate_days=0 + ) if queue_slugs is not None: queues = queues.filter(slug__in=queue_slugs) @@ -68,17 +70,15 @@ class Command(BaseCommand): req_last_escl_date = timezone.now() - timedelta(days=days) - for ticket in queue.ticket_set.filter( - status__in=Ticket.OPEN_STATUSES - ).exclude( - priority=1 - ).filter( - Q(on_hold__isnull=True) | Q(on_hold=False) - ).filter( - Q(last_escalation__lte=req_last_escl_date) | - Q(last_escalation__isnull=True, created__lte=req_last_escl_date) + for ticket in ( + queue.ticket_set.filter(status__in=Ticket.OPEN_STATUSES) + .exclude(priority=1) + .filter(Q(on_hold__isnull=True) | Q(on_hold=False)) + .filter( + Q(last_escalation__lte=req_last_escl_date) + | Q(last_escalation__isnull=True, created__lte=req_last_escl_date) + ) ): - ticket.last_escalation = timezone.now() ticket.priority -= 1 ticket.save() @@ -86,24 +86,29 @@ class Command(BaseCommand): context = safe_template_context(ticket) ticket.send( - {'submitter': ('escalated_submitter', context), - 'ticket_cc': ('escalated_cc', context), - 'assigned_to': ('escalated_owner', context)}, + { + "submitter": ("escalated_submitter", context), + "ticket_cc": ("escalated_cc", context), + "assigned_to": ("escalated_owner", context), + }, fail_silently=True, ) if verbose: - self.stdout.write(f" - Esclating {ticket.ticket} from {ticket.priority + 1}>{ticket.priority}") + self.stdout.write( + f" - Esclating {ticket.ticket} from {ticket.priority + 1}>{ticket.priority}" + ) if not notify_only: followup = ticket.followup_set.create( - title=_('Ticket Escalated'), + title=_("Ticket Escalated"), public=True, - comment=_('Ticket escalated after %(nb)s days') % {'nb': queue.escalate_days}, + comment=_("Ticket escalated after %(nb)s days") + % {"nb": queue.escalate_days}, ) followup.ticketchange_set.create( - field=_('Priority'), + field=_("Priority"), old_value=ticket.priority + 1, new_value=ticket.priority, ) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index a0cb3726..b288811d 100755 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -10,36 +10,38 @@ scripts/get_email.py - Designed to be run from cron, this script checks the helpdesk, creating tickets from the new messages (or adding to existing tickets if needed) """ + from django.core.management.base import BaseCommand from helpdesk.email import process_email class Command(BaseCommand): - - help = 'Process django-helpdesk queues and process e-mails via POP3/IMAP or ' \ - 'from a local mailbox directory as required, feeding them into the helpdesk.' + help = ( + "Process django-helpdesk queues and process e-mails via POP3/IMAP or " + "from a local mailbox directory as required, feeding them into the helpdesk." + ) def add_arguments(self, parser): parser.add_argument( - '--quiet', - action='store_true', - dest='quiet', + "--quiet", + action="store_true", + dest="quiet", default=False, - help='Hide details about each queue/message as they are processed', + help="Hide details about each queue/message as they are processed", ) parser.add_argument( - '--debug_to_stdout', - action='store_true', - dest='debug_to_stdout', + "--debug_to_stdout", + action="store_true", + dest="debug_to_stdout", default=False, - help='Log additional messaging to stdout.', + help="Log additional messaging to stdout.", ) def handle(self, *args, **options): - quiet = options.get('quiet') - debug_to_stdout = options.get('debug_to_stdout') + quiet = options.get("quiet") + debug_to_stdout = options.get("debug_to_stdout") process_email(quiet=quiet, debug_to_stdout=debug_to_stdout) -if __name__ == '__main__': +if __name__ == "__main__": process_email() diff --git a/helpdesk/migrations/0001_initial.py b/helpdesk/migrations/0001_initial.py index 8ae60e4a..6c8e96e2 100644 --- a/helpdesk/migrations/0001_initial.py +++ b/helpdesk/migrations/0001_initial.py @@ -7,342 +7,1147 @@ import helpdesk.models class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Attachment', + name="Attachment", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('file', models.FileField(upload_to=helpdesk.models.attachment_path, verbose_name='File', max_length=1000)), - ('filename', models.CharField(verbose_name='Filename', max_length=1000)), - ('mime_type', models.CharField(verbose_name='MIME Type', max_length=255)), - ('size', models.IntegerField(verbose_name='Size', help_text='Size of this file in bytes')), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ( + "file", + models.FileField( + upload_to=helpdesk.models.attachment_path, + verbose_name="File", + max_length=1000, + ), + ), + ( + "filename", + models.CharField(verbose_name="Filename", max_length=1000), + ), + ( + "mime_type", + models.CharField(verbose_name="MIME Type", max_length=255), + ), + ( + "size", + models.IntegerField( + verbose_name="Size", help_text="Size of this file in bytes" + ), + ), ], options={ - 'verbose_name_plural': 'Attachments', - 'verbose_name': 'Attachment', - 'ordering': ['filename'], + "verbose_name_plural": "Attachments", + "verbose_name": "Attachment", + "ordering": ["filename"], }, bases=(models.Model,), ), migrations.CreateModel( - name='CustomField', + name="CustomField", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('name', models.SlugField(help_text='As used in the database and behind the scenes. Must be unique and consist of only lowercase letters with no punctuation.', verbose_name='Field Name', unique=True)), - ('label', models.CharField(verbose_name='Label', help_text='The display label for this field', max_length='30')), - ('help_text', models.TextField(null=True, verbose_name='Help Text', blank=True, help_text='Shown to the user when editing the ticket')), - ('data_type', models.CharField(choices=[('varchar', 'Character (single line)'), ('text', 'Text (multi-line)'), ('integer', 'Integer'), ('decimal', 'Decimal'), ('list', 'List'), ('boolean', 'Boolean (checkbox yes/no)'), ('date', 'Date'), ('time', 'Time'), ('datetime', 'Date & Time'), ('email', 'E-Mail Address'), ('url', 'URL'), ('ipaddress', 'IP Address'), ('slug', 'Slug')], verbose_name='Data Type', help_text='Allows you to restrict the data entered into this field', max_length=100)), - ('max_length', models.IntegerField(null=True, verbose_name='Maximum Length (characters)', blank=True)), - ('decimal_places', models.IntegerField(null=True, verbose_name='Decimal Places', blank=True, help_text='Only used for decimal fields')), - ('empty_selection_list', models.BooleanField(verbose_name='Add empty first choice to List?', default=False, help_text='Only for List: adds an empty first entry to the choices list, which enforces that the user makes an active choice.')), - ('list_values', models.TextField(null=True, verbose_name='List Values', blank=True, help_text='For list fields only. Enter one option per line.')), - ('ordering', models.IntegerField(null=True, verbose_name='Ordering', blank=True, help_text='Lower numbers are displayed first; higher numbers are listed later')), - ('required', models.BooleanField(verbose_name='Required?', default=False, help_text='Does the user have to enter a value for this field?')), - ('staff_only', models.BooleanField(verbose_name='Staff Only?', default=False, help_text='If this is ticked, then the public submission form will NOT show this field')), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ( + "name", + models.SlugField( + help_text="As used in the database and behind the scenes. Must be unique and consist of only lowercase letters with no punctuation.", + verbose_name="Field Name", + unique=True, + ), + ), + ( + "label", + models.CharField( + verbose_name="Label", + help_text="The display label for this field", + max_length="30", + ), + ), + ( + "help_text", + models.TextField( + null=True, + verbose_name="Help Text", + blank=True, + help_text="Shown to the user when editing the ticket", + ), + ), + ( + "data_type", + models.CharField( + choices=[ + ("varchar", "Character (single line)"), + ("text", "Text (multi-line)"), + ("integer", "Integer"), + ("decimal", "Decimal"), + ("list", "List"), + ("boolean", "Boolean (checkbox yes/no)"), + ("date", "Date"), + ("time", "Time"), + ("datetime", "Date & Time"), + ("email", "E-Mail Address"), + ("url", "URL"), + ("ipaddress", "IP Address"), + ("slug", "Slug"), + ], + verbose_name="Data Type", + help_text="Allows you to restrict the data entered into this field", + max_length=100, + ), + ), + ( + "max_length", + models.IntegerField( + null=True, + verbose_name="Maximum Length (characters)", + blank=True, + ), + ), + ( + "decimal_places", + models.IntegerField( + null=True, + verbose_name="Decimal Places", + blank=True, + help_text="Only used for decimal fields", + ), + ), + ( + "empty_selection_list", + models.BooleanField( + verbose_name="Add empty first choice to List?", + default=False, + help_text="Only for List: adds an empty first entry to the choices list, which enforces that the user makes an active choice.", + ), + ), + ( + "list_values", + models.TextField( + null=True, + verbose_name="List Values", + blank=True, + help_text="For list fields only. Enter one option per line.", + ), + ), + ( + "ordering", + models.IntegerField( + null=True, + verbose_name="Ordering", + blank=True, + help_text="Lower numbers are displayed first; higher numbers are listed later", + ), + ), + ( + "required", + models.BooleanField( + verbose_name="Required?", + default=False, + help_text="Does the user have to enter a value for this field?", + ), + ), + ( + "staff_only", + models.BooleanField( + verbose_name="Staff Only?", + default=False, + help_text="If this is ticked, then the public submission form will NOT show this field", + ), + ), ], options={ - 'verbose_name_plural': 'Custom fields', - 'verbose_name': 'Custom field', + "verbose_name_plural": "Custom fields", + "verbose_name": "Custom field", }, bases=(models.Model,), ), migrations.CreateModel( - name='EmailTemplate', + name="EmailTemplate", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('template_name', models.CharField(verbose_name='Template Name', max_length=100)), - ('subject', models.CharField(verbose_name='Subject', help_text='This will be prefixed with "[ticket.ticket] ticket.title". We recommend something simple such as "(Updated") or "(Closed)" - the same context is available as in plain_text, below.', max_length=100)), - ('heading', models.CharField(verbose_name='Heading', help_text='In HTML e-mails, this will be the heading at the top of the email - the same context is available as in plain_text, below.', max_length=100)), - ('plain_text', models.TextField(verbose_name='Plain Text', help_text='The context available to you includes {{ ticket }}, {{ queue }}, and depending on the time of the call: {{ resolution }} or {{ comment }}.')), - ('html', models.TextField(verbose_name='HTML', help_text='The same context is available here as in plain_text, above.')), - ('locale', models.CharField(null=True, verbose_name='Locale', help_text='Locale of this template.', blank=True, max_length=10)), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ( + "template_name", + models.CharField(verbose_name="Template Name", max_length=100), + ), + ( + "subject", + models.CharField( + verbose_name="Subject", + help_text='This will be prefixed with "[ticket.ticket] ticket.title". We recommend something simple such as "(Updated") or "(Closed)" - the same context is available as in plain_text, below.', + max_length=100, + ), + ), + ( + "heading", + models.CharField( + verbose_name="Heading", + help_text="In HTML e-mails, this will be the heading at the top of the email - the same context is available as in plain_text, below.", + max_length=100, + ), + ), + ( + "plain_text", + models.TextField( + verbose_name="Plain Text", + help_text="The context available to you includes {{ ticket }}, {{ queue }}, and depending on the time of the call: {{ resolution }} or {{ comment }}.", + ), + ), + ( + "html", + models.TextField( + verbose_name="HTML", + help_text="The same context is available here as in plain_text, above.", + ), + ), + ( + "locale", + models.CharField( + null=True, + verbose_name="Locale", + help_text="Locale of this template.", + blank=True, + max_length=10, + ), + ), ], options={ - 'verbose_name_plural': 'e-mail templates', - 'verbose_name': 'e-mail template', - 'ordering': ['template_name', 'locale'], + "verbose_name_plural": "e-mail templates", + "verbose_name": "e-mail template", + "ordering": ["template_name", "locale"], }, bases=(models.Model,), ), migrations.CreateModel( - name='EscalationExclusion', + name="EscalationExclusion", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('name', models.CharField(verbose_name='Name', max_length=100)), - ('date', models.DateField(verbose_name='Date', help_text='Date on which escalation should not happen')), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ("name", models.CharField(verbose_name="Name", max_length=100)), + ( + "date", + models.DateField( + verbose_name="Date", + help_text="Date on which escalation should not happen", + ), + ), ], options={ - 'verbose_name_plural': 'Escalation exclusions', - 'verbose_name': 'Escalation exclusion', + "verbose_name_plural": "Escalation exclusions", + "verbose_name": "Escalation exclusion", }, bases=(models.Model,), ), migrations.CreateModel( - name='FollowUp', + name="FollowUp", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('date', models.DateTimeField(verbose_name='Date', default=django.utils.timezone.now)), - ('title', models.CharField(null=True, verbose_name='Title', blank=True, max_length=200)), - ('comment', models.TextField(null=True, verbose_name='Comment', blank=True)), - ('public', models.BooleanField(verbose_name='Public', default=False, help_text='Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.')), - ('new_status', models.IntegerField(null=True, verbose_name='New Status', help_text='If the status was changed, what was it changed to?', blank=True, choices=[(1, 'Open'), (2, 'Reopened'), (3, 'Resolved'), (4, 'Closed'), (5, 'Duplicate')])), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ( + "date", + models.DateTimeField( + verbose_name="Date", default=django.utils.timezone.now + ), + ), + ( + "title", + models.CharField( + null=True, verbose_name="Title", blank=True, max_length=200 + ), + ), + ( + "comment", + models.TextField(null=True, verbose_name="Comment", blank=True), + ), + ( + "public", + models.BooleanField( + verbose_name="Public", + default=False, + help_text="Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.", + ), + ), + ( + "new_status", + models.IntegerField( + null=True, + verbose_name="New Status", + help_text="If the status was changed, what was it changed to?", + blank=True, + choices=[ + (1, "Open"), + (2, "Reopened"), + (3, "Resolved"), + (4, "Closed"), + (5, "Duplicate"), + ], + ), + ), ], options={ - 'verbose_name_plural': 'Follow-ups', - 'verbose_name': 'Follow-up', - 'ordering': ['date'], + "verbose_name_plural": "Follow-ups", + "verbose_name": "Follow-up", + "ordering": ["date"], }, bases=(models.Model,), ), migrations.CreateModel( - name='IgnoreEmail', + name="IgnoreEmail", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('name', models.CharField(verbose_name='Name', max_length=100)), - ('date', models.DateField(editable=False, verbose_name='Date', blank=True, help_text='Date on which this e-mail address was added')), - ('email_address', models.CharField(verbose_name='E-Mail Address', help_text='Enter a full e-mail address, or portions with wildcards, eg *@domain.com or postmaster@*.', max_length=150)), - ('keep_in_mailbox', models.BooleanField(verbose_name='Save Emails in Mailbox?', default=False, help_text='Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.')), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ("name", models.CharField(verbose_name="Name", max_length=100)), + ( + "date", + models.DateField( + editable=False, + verbose_name="Date", + blank=True, + help_text="Date on which this e-mail address was added", + ), + ), + ( + "email_address", + models.CharField( + verbose_name="E-Mail Address", + help_text="Enter a full e-mail address, or portions with wildcards, eg *@domain.com or postmaster@*.", + max_length=150, + ), + ), + ( + "keep_in_mailbox", + models.BooleanField( + verbose_name="Save Emails in Mailbox?", + default=False, + help_text="Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.", + ), + ), ], options={ - 'verbose_name_plural': 'Ignored e-mail addresses', - 'verbose_name': 'Ignored e-mail address', + "verbose_name_plural": "Ignored e-mail addresses", + "verbose_name": "Ignored e-mail address", }, bases=(models.Model,), ), migrations.CreateModel( - name='KBCategory', + name="KBCategory", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('title', models.CharField(verbose_name='Title', max_length=100)), - ('slug', models.SlugField(verbose_name='Slug')), - ('description', models.TextField(verbose_name='Description')), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ("title", models.CharField(verbose_name="Title", max_length=100)), + ("slug", models.SlugField(verbose_name="Slug")), + ("description", models.TextField(verbose_name="Description")), ], options={ - 'verbose_name_plural': 'Knowledge base categories', - 'verbose_name': 'Knowledge base category', - 'ordering': ['title'], + "verbose_name_plural": "Knowledge base categories", + "verbose_name": "Knowledge base category", + "ordering": ["title"], }, bases=(models.Model,), ), migrations.CreateModel( - name='KBItem', + name="KBItem", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('title', models.CharField(verbose_name='Title', max_length=100)), - ('question', models.TextField(verbose_name='Question')), - ('answer', models.TextField(verbose_name='Answer')), - ('votes', models.IntegerField(verbose_name='Votes', default=0, help_text='Total number of votes cast for this item')), - ('recommendations', models.IntegerField(verbose_name='Positive Votes', default=0, help_text='Number of votes for this item which were POSITIVE.')), - ('last_updated', models.DateTimeField(verbose_name='Last Updated', blank=True, help_text='The date on which this question was most recently changed.')), - ('category', models.ForeignKey(verbose_name='Category', to='helpdesk.KBCategory', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ("title", models.CharField(verbose_name="Title", max_length=100)), + ("question", models.TextField(verbose_name="Question")), + ("answer", models.TextField(verbose_name="Answer")), + ( + "votes", + models.IntegerField( + verbose_name="Votes", + default=0, + help_text="Total number of votes cast for this item", + ), + ), + ( + "recommendations", + models.IntegerField( + verbose_name="Positive Votes", + default=0, + help_text="Number of votes for this item which were POSITIVE.", + ), + ), + ( + "last_updated", + models.DateTimeField( + verbose_name="Last Updated", + blank=True, + help_text="The date on which this question was most recently changed.", + ), + ), + ( + "category", + models.ForeignKey( + verbose_name="Category", + to="helpdesk.KBCategory", + on_delete=models.CASCADE, + ), + ), ], options={ - 'verbose_name_plural': 'Knowledge base items', - 'verbose_name': 'Knowledge base item', - 'ordering': ['title'], + "verbose_name_plural": "Knowledge base items", + "verbose_name": "Knowledge base item", + "ordering": ["title"], }, bases=(models.Model,), ), migrations.CreateModel( - name='PreSetReply', + name="PreSetReply", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('name', models.CharField(verbose_name='Name', help_text='Only used to assist users with selecting a reply - not shown to the user.', max_length=100)), - ('body', models.TextField(verbose_name='Body', help_text='Context available: {{ ticket }} - ticket object (eg {{ ticket.title }}); {{ queue }} - The queue; and {{ user }} - the current user.')), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ( + "name", + models.CharField( + verbose_name="Name", + help_text="Only used to assist users with selecting a reply - not shown to the user.", + max_length=100, + ), + ), + ( + "body", + models.TextField( + verbose_name="Body", + help_text="Context available: {{ ticket }} - ticket object (eg {{ ticket.title }}); {{ queue }} - The queue; and {{ user }} - the current user.", + ), + ), ], options={ - 'verbose_name_plural': 'Pre-set replies', - 'verbose_name': 'Pre-set reply', - 'ordering': ['name'], + "verbose_name_plural": "Pre-set replies", + "verbose_name": "Pre-set reply", + "ordering": ["name"], }, bases=(models.Model,), ), migrations.CreateModel( - name='Queue', + name="Queue", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('title', models.CharField(verbose_name='Title', max_length=100)), - ('slug', models.SlugField(verbose_name='Slug', help_text="This slug is used when building ticket ID's. Once set, try not to change it or e-mailing may get messy.")), - ('email_address', models.EmailField(null=True, verbose_name='E-Mail Address', help_text='All outgoing e-mails for this queue will use this e-mail address. If you use IMAP or POP3, this should be the e-mail address for that mailbox.', blank=True, max_length=75)), - ('locale', models.CharField(null=True, verbose_name='Locale', help_text='Locale of this queue. All correspondence in this queue will be in this language.', blank=True, max_length=10)), - ('allow_public_submission', models.BooleanField(verbose_name='Allow Public Submission?', default=False, help_text='Should this queue be listed on the public submission form?')), - ('allow_email_submission', models.BooleanField(verbose_name='Allow E-Mail Submission?', default=False, help_text='Do you want to poll the e-mail box below for new tickets?')), - ('escalate_days', models.IntegerField(null=True, verbose_name='Escalation Days', blank=True, help_text='For tickets which are not held, how often do you wish to increase their priority? Set to 0 for no escalation.')), - ('new_ticket_cc', models.CharField(null=True, verbose_name='New Ticket CC Address', help_text='If an e-mail address is entered here, then it will receive notification of all new tickets created for this queue. Enter a comma between multiple e-mail addresses.', blank=True, max_length=200)), - ('updated_ticket_cc', models.CharField(null=True, verbose_name='Updated Ticket CC Address', help_text='If an e-mail address is entered here, then it will receive notification of all activity (new tickets, closed tickets, updates, reassignments, etc) for this queue. Separate multiple addresses with a comma.', blank=True, max_length=200)), - ('email_box_type', models.CharField(null=True, verbose_name='E-Mail Box Type', help_text='E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported.', blank=True, max_length=5, choices=[('pop3', 'POP 3'), ('imap', 'IMAP')])), - ('email_box_host', models.CharField(null=True, verbose_name='E-Mail Hostname', help_text='Your e-mail server address - either the domain name or IP address. May be "localhost".', blank=True, max_length=200)), - ('email_box_port', models.IntegerField(null=True, verbose_name='E-Mail Port', blank=True, help_text='Port number to use for accessing e-mail. Default for POP3 is "110", and for IMAP is "143". This may differ on some servers. Leave it blank to use the defaults.')), - ('email_box_ssl', models.BooleanField(verbose_name='Use SSL for E-Mail?', default=False, help_text='Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.')), - ('email_box_user', models.CharField(null=True, verbose_name='E-Mail Username', help_text='Username for accessing this mailbox.', blank=True, max_length=200)), - ('email_box_pass', models.CharField(null=True, verbose_name='E-Mail Password', help_text='Password for the above username', blank=True, max_length=200)), - ('email_box_imap_folder', models.CharField(null=True, verbose_name='IMAP Folder', help_text='If using IMAP, what folder do you wish to fetch messages from? This allows you to use one IMAP account for multiple queues, by filtering messages on your IMAP server into separate folders. Default: INBOX.', blank=True, max_length=100)), - ('email_box_interval', models.IntegerField(null=True, verbose_name='E-Mail Check Interval', blank=True, default='5', help_text='How often do you wish to check this mailbox? (in Minutes)')), - ('email_box_last_check', models.DateTimeField(editable=False, null=True, blank=True)), - ('socks_proxy_type', models.CharField(null=True, verbose_name='Socks Proxy Type', help_text='SOCKS4 or SOCKS5 allows you to proxy your connections through a SOCKS server.', blank=True, max_length=8, choices=[('socks4', 'SOCKS4'), ('socks5', 'SOCKS5')])), - ('socks_proxy_host', models.GenericIPAddressField(null=True, verbose_name='Socks Proxy Host', help_text='Socks proxy IP address. Default: 127.0.0.1', blank=True)), - ('socks_proxy_port', models.IntegerField(null=True, verbose_name='Socks Proxy Port', blank=True, help_text='Socks proxy port number. Default: 9150 (default TOR port)')), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ("title", models.CharField(verbose_name="Title", max_length=100)), + ( + "slug", + models.SlugField( + verbose_name="Slug", + help_text="This slug is used when building ticket ID's. Once set, try not to change it or e-mailing may get messy.", + ), + ), + ( + "email_address", + models.EmailField( + null=True, + verbose_name="E-Mail Address", + help_text="All outgoing e-mails for this queue will use this e-mail address. If you use IMAP or POP3, this should be the e-mail address for that mailbox.", + blank=True, + max_length=75, + ), + ), + ( + "locale", + models.CharField( + null=True, + verbose_name="Locale", + help_text="Locale of this queue. All correspondence in this queue will be in this language.", + blank=True, + max_length=10, + ), + ), + ( + "allow_public_submission", + models.BooleanField( + verbose_name="Allow Public Submission?", + default=False, + help_text="Should this queue be listed on the public submission form?", + ), + ), + ( + "allow_email_submission", + models.BooleanField( + verbose_name="Allow E-Mail Submission?", + default=False, + help_text="Do you want to poll the e-mail box below for new tickets?", + ), + ), + ( + "escalate_days", + models.IntegerField( + null=True, + verbose_name="Escalation Days", + blank=True, + help_text="For tickets which are not held, how often do you wish to increase their priority? Set to 0 for no escalation.", + ), + ), + ( + "new_ticket_cc", + models.CharField( + null=True, + verbose_name="New Ticket CC Address", + help_text="If an e-mail address is entered here, then it will receive notification of all new tickets created for this queue. Enter a comma between multiple e-mail addresses.", + blank=True, + max_length=200, + ), + ), + ( + "updated_ticket_cc", + models.CharField( + null=True, + verbose_name="Updated Ticket CC Address", + help_text="If an e-mail address is entered here, then it will receive notification of all activity (new tickets, closed tickets, updates, reassignments, etc) for this queue. Separate multiple addresses with a comma.", + blank=True, + max_length=200, + ), + ), + ( + "email_box_type", + models.CharField( + null=True, + verbose_name="E-Mail Box Type", + help_text="E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported.", + blank=True, + max_length=5, + choices=[("pop3", "POP 3"), ("imap", "IMAP")], + ), + ), + ( + "email_box_host", + models.CharField( + null=True, + verbose_name="E-Mail Hostname", + help_text='Your e-mail server address - either the domain name or IP address. May be "localhost".', + blank=True, + max_length=200, + ), + ), + ( + "email_box_port", + models.IntegerField( + null=True, + verbose_name="E-Mail Port", + blank=True, + help_text='Port number to use for accessing e-mail. Default for POP3 is "110", and for IMAP is "143". This may differ on some servers. Leave it blank to use the defaults.', + ), + ), + ( + "email_box_ssl", + models.BooleanField( + verbose_name="Use SSL for E-Mail?", + default=False, + help_text="Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.", + ), + ), + ( + "email_box_user", + models.CharField( + null=True, + verbose_name="E-Mail Username", + help_text="Username for accessing this mailbox.", + blank=True, + max_length=200, + ), + ), + ( + "email_box_pass", + models.CharField( + null=True, + verbose_name="E-Mail Password", + help_text="Password for the above username", + blank=True, + max_length=200, + ), + ), + ( + "email_box_imap_folder", + models.CharField( + null=True, + verbose_name="IMAP Folder", + help_text="If using IMAP, what folder do you wish to fetch messages from? This allows you to use one IMAP account for multiple queues, by filtering messages on your IMAP server into separate folders. Default: INBOX.", + blank=True, + max_length=100, + ), + ), + ( + "email_box_interval", + models.IntegerField( + null=True, + verbose_name="E-Mail Check Interval", + blank=True, + default="5", + help_text="How often do you wish to check this mailbox? (in Minutes)", + ), + ), + ( + "email_box_last_check", + models.DateTimeField(editable=False, null=True, blank=True), + ), + ( + "socks_proxy_type", + models.CharField( + null=True, + verbose_name="Socks Proxy Type", + help_text="SOCKS4 or SOCKS5 allows you to proxy your connections through a SOCKS server.", + blank=True, + max_length=8, + choices=[("socks4", "SOCKS4"), ("socks5", "SOCKS5")], + ), + ), + ( + "socks_proxy_host", + models.GenericIPAddressField( + null=True, + verbose_name="Socks Proxy Host", + help_text="Socks proxy IP address. Default: 127.0.0.1", + blank=True, + ), + ), + ( + "socks_proxy_port", + models.IntegerField( + null=True, + verbose_name="Socks Proxy Port", + blank=True, + help_text="Socks proxy port number. Default: 9150 (default TOR port)", + ), + ), ], options={ - 'verbose_name_plural': 'Queues', - 'verbose_name': 'Queue', - 'ordering': ('title',), + "verbose_name_plural": "Queues", + "verbose_name": "Queue", + "ordering": ("title",), }, bases=(models.Model,), ), migrations.CreateModel( - name='SavedSearch', + name="SavedSearch", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('title', models.CharField(verbose_name='Query Name', help_text='User-provided name for this query', max_length=100)), - ('shared', models.BooleanField(verbose_name='Shared With Other Users?', default=False, help_text='Should other users see this query?')), - ('query', models.TextField(verbose_name='Search Query', help_text='Pickled query object. Be wary changing this.')), - ('user', models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ( + "title", + models.CharField( + verbose_name="Query Name", + help_text="User-provided name for this query", + max_length=100, + ), + ), + ( + "shared", + models.BooleanField( + verbose_name="Shared With Other Users?", + default=False, + help_text="Should other users see this query?", + ), + ), + ( + "query", + models.TextField( + verbose_name="Search Query", + help_text="Pickled query object. Be wary changing this.", + ), + ), + ( + "user", + models.ForeignKey( + verbose_name="User", + to=settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ), + ), ], options={ - 'verbose_name_plural': 'Saved searches', - 'verbose_name': 'Saved search', + "verbose_name_plural": "Saved searches", + "verbose_name": "Saved search", }, bases=(models.Model,), ), migrations.CreateModel( - name='Ticket', + name="Ticket", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('title', models.CharField(verbose_name='Title', max_length=200)), - ('created', models.DateTimeField(verbose_name='Created', blank=True, help_text='Date this ticket was first created')), - ('modified', models.DateTimeField(verbose_name='Modified', blank=True, help_text='Date this ticket was most recently changed.')), - ('submitter_email', models.EmailField(null=True, verbose_name='Submitter E-Mail', help_text='The submitter will receive an email for all public follow-ups left for this task.', blank=True, max_length=75)), - ('status', models.IntegerField(verbose_name='Status', default=1, choices=[(1, 'Open'), (2, 'Reopened'), (3, 'Resolved'), (4, 'Closed'), (5, 'Duplicate')])), - ('on_hold', models.BooleanField(verbose_name='On Hold', default=False, help_text='If a ticket is on hold, it will not automatically be escalated.')), - ('description', models.TextField(null=True, verbose_name='Description', blank=True, help_text='The content of the customers query.')), - ('resolution', models.TextField(null=True, verbose_name='Resolution', blank=True, help_text='The resolution provided to the customer by our staff.')), - ('priority', models.IntegerField(verbose_name='Priority', help_text='1 = Highest Priority, 5 = Low Priority', blank=3, default=3, choices=[(1, '1. Critical'), (2, '2. High'), (3, '3. Normal'), (4, '4. Low'), (5, '5. Very Low')])), - ('due_date', models.DateTimeField(null=True, verbose_name='Due on', blank=True)), - ('last_escalation', models.DateTimeField(editable=False, null=True, blank=True, help_text='The date this ticket was last escalated - updated automatically by management/commands/escalate_tickets.py.')), - ('assigned_to', models.ForeignKey(null=True, verbose_name='Assigned to', blank=True, related_name='assigned_to', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), - ('queue', models.ForeignKey(verbose_name='Queue', to='helpdesk.Queue', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ("title", models.CharField(verbose_name="Title", max_length=200)), + ( + "created", + models.DateTimeField( + verbose_name="Created", + blank=True, + help_text="Date this ticket was first created", + ), + ), + ( + "modified", + models.DateTimeField( + verbose_name="Modified", + blank=True, + help_text="Date this ticket was most recently changed.", + ), + ), + ( + "submitter_email", + models.EmailField( + null=True, + verbose_name="Submitter E-Mail", + help_text="The submitter will receive an email for all public follow-ups left for this task.", + blank=True, + max_length=75, + ), + ), + ( + "status", + models.IntegerField( + verbose_name="Status", + default=1, + choices=[ + (1, "Open"), + (2, "Reopened"), + (3, "Resolved"), + (4, "Closed"), + (5, "Duplicate"), + ], + ), + ), + ( + "on_hold", + models.BooleanField( + verbose_name="On Hold", + default=False, + help_text="If a ticket is on hold, it will not automatically be escalated.", + ), + ), + ( + "description", + models.TextField( + null=True, + verbose_name="Description", + blank=True, + help_text="The content of the customers query.", + ), + ), + ( + "resolution", + models.TextField( + null=True, + verbose_name="Resolution", + blank=True, + help_text="The resolution provided to the customer by our staff.", + ), + ), + ( + "priority", + models.IntegerField( + verbose_name="Priority", + help_text="1 = Highest Priority, 5 = Low Priority", + blank=3, + default=3, + choices=[ + (1, "1. Critical"), + (2, "2. High"), + (3, "3. Normal"), + (4, "4. Low"), + (5, "5. Very Low"), + ], + ), + ), + ( + "due_date", + models.DateTimeField(null=True, verbose_name="Due on", blank=True), + ), + ( + "last_escalation", + models.DateTimeField( + editable=False, + null=True, + blank=True, + help_text="The date this ticket was last escalated - updated automatically by management/commands/escalate_tickets.py.", + ), + ), + ( + "assigned_to", + models.ForeignKey( + null=True, + verbose_name="Assigned to", + blank=True, + related_name="assigned_to", + to=settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ), + ), + ( + "queue", + models.ForeignKey( + verbose_name="Queue", + to="helpdesk.Queue", + on_delete=models.CASCADE, + ), + ), ], options={ - 'verbose_name_plural': 'Tickets', - 'verbose_name': 'Ticket', - 'ordering': ('id',), - 'get_latest_by': 'created', + "verbose_name_plural": "Tickets", + "verbose_name": "Ticket", + "ordering": ("id",), + "get_latest_by": "created", }, bases=(models.Model,), ), migrations.CreateModel( - name='TicketCC', + name="TicketCC", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('email', models.EmailField(null=True, verbose_name='E-Mail Address', help_text='For non-user followers, enter their e-mail address', blank=True, max_length=75)), - ('can_view', models.BooleanField(verbose_name='Can View Ticket?', default=False, help_text='Can this CC login to view the ticket details?')), - ('can_update', models.BooleanField(verbose_name='Can Update Ticket?', default=False, help_text='Can this CC login and update the ticket?')), - ('ticket', models.ForeignKey(verbose_name='Ticket', to='helpdesk.Ticket', on_delete=models.CASCADE)), - ('user', models.ForeignKey(null=True, verbose_name='User', blank=True, to=settings.AUTH_USER_MODEL, help_text='User who wishes to receive updates for this ticket.', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ( + "email", + models.EmailField( + null=True, + verbose_name="E-Mail Address", + help_text="For non-user followers, enter their e-mail address", + blank=True, + max_length=75, + ), + ), + ( + "can_view", + models.BooleanField( + verbose_name="Can View Ticket?", + default=False, + help_text="Can this CC login to view the ticket details?", + ), + ), + ( + "can_update", + models.BooleanField( + verbose_name="Can Update Ticket?", + default=False, + help_text="Can this CC login and update the ticket?", + ), + ), + ( + "ticket", + models.ForeignKey( + verbose_name="Ticket", + to="helpdesk.Ticket", + on_delete=models.CASCADE, + ), + ), + ( + "user", + models.ForeignKey( + null=True, + verbose_name="User", + blank=True, + to=settings.AUTH_USER_MODEL, + help_text="User who wishes to receive updates for this ticket.", + on_delete=models.CASCADE, + ), + ), + ], + options={}, + bases=(models.Model,), + ), + migrations.CreateModel( + name="TicketChange", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ("field", models.CharField(verbose_name="Field", max_length=100)), + ( + "old_value", + models.TextField(null=True, verbose_name="Old Value", blank=True), + ), + ( + "new_value", + models.TextField(null=True, verbose_name="New Value", blank=True), + ), + ( + "followup", + models.ForeignKey( + verbose_name="Follow-up", + to="helpdesk.FollowUp", + on_delete=models.CASCADE, + ), + ), ], options={ + "verbose_name_plural": "Ticket changes", + "verbose_name": "Ticket change", }, bases=(models.Model,), ), migrations.CreateModel( - name='TicketChange', + name="TicketCustomFieldValue", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('field', models.CharField(verbose_name='Field', max_length=100)), - ('old_value', models.TextField(null=True, verbose_name='Old Value', blank=True)), - ('new_value', models.TextField(null=True, verbose_name='New Value', blank=True)), - ('followup', models.ForeignKey(verbose_name='Follow-up', to='helpdesk.FollowUp', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ("value", models.TextField(null=True, blank=True)), + ( + "field", + models.ForeignKey( + verbose_name="Field", + to="helpdesk.CustomField", + on_delete=models.CASCADE, + ), + ), + ( + "ticket", + models.ForeignKey( + verbose_name="Ticket", + to="helpdesk.Ticket", + on_delete=models.CASCADE, + ), + ), ], options={ - 'verbose_name_plural': 'Ticket changes', - 'verbose_name': 'Ticket change', + "verbose_name_plural": "Ticket custom field values", + "verbose_name": "Ticket custom field value", }, bases=(models.Model,), ), migrations.CreateModel( - name='TicketCustomFieldValue', + name="TicketDependency", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('value', models.TextField(null=True, blank=True)), - ('field', models.ForeignKey(verbose_name='Field', to='helpdesk.CustomField', on_delete=models.CASCADE)), - ('ticket', models.ForeignKey(verbose_name='Ticket', to='helpdesk.Ticket', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ( + "depends_on", + models.ForeignKey( + related_name="depends_on", + verbose_name="Depends On Ticket", + to="helpdesk.Ticket", + on_delete=models.CASCADE, + ), + ), + ( + "ticket", + models.ForeignKey( + related_name="ticketdependency", + verbose_name="Ticket", + to="helpdesk.Ticket", + on_delete=models.CASCADE, + ), + ), ], options={ - 'verbose_name_plural': 'Ticket custom field values', - 'verbose_name': 'Ticket custom field value', + "verbose_name_plural": "Ticket dependencies", + "verbose_name": "Ticket dependency", }, bases=(models.Model,), ), migrations.CreateModel( - name='TicketDependency', + name="UserSettings", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('depends_on', models.ForeignKey(related_name='depends_on', verbose_name='Depends On Ticket', to='helpdesk.Ticket', on_delete=models.CASCADE)), - ('ticket', models.ForeignKey(related_name='ticketdependency', verbose_name='Ticket', to='helpdesk.Ticket', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ( + "settings_pickled", + models.TextField( + null=True, + verbose_name="Settings Dictionary", + blank=True, + help_text="This is a base64-encoded representation of a pickled Python dictionary. Do not change this field via the admin.", + ), + ), + ( + "user", + models.OneToOneField( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), + ), ], options={ - 'verbose_name_plural': 'Ticket dependencies', - 'verbose_name': 'Ticket dependency', - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='UserSettings', - fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('settings_pickled', models.TextField(null=True, verbose_name='Settings Dictionary', blank=True, help_text='This is a base64-encoded representation of a pickled Python dictionary. Do not change this field via the admin.')), - ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), - ], - options={ - 'verbose_name_plural': 'User Settings', - 'verbose_name': 'User Setting', + "verbose_name_plural": "User Settings", + "verbose_name": "User Setting", }, bases=(models.Model,), ), migrations.AlterUniqueTogether( - name='ticketdependency', - unique_together=set([('ticket', 'depends_on')]), + name="ticketdependency", + unique_together=set([("ticket", "depends_on")]), ), migrations.AddField( - model_name='presetreply', - name='queues', - field=models.ManyToManyField(null=True, to='helpdesk.Queue', blank=True, help_text='Leave blank to allow this reply to be used for all queues, or select those queues you wish to limit this reply to.'), + model_name="presetreply", + name="queues", + field=models.ManyToManyField( + null=True, + to="helpdesk.Queue", + blank=True, + help_text="Leave blank to allow this reply to be used for all queues, or select those queues you wish to limit this reply to.", + ), preserve_default=True, ), migrations.AddField( - model_name='ignoreemail', - name='queues', - field=models.ManyToManyField(null=True, to='helpdesk.Queue', blank=True, help_text='Leave blank for this e-mail to be ignored on all queues, or select those queues you wish to ignore this e-mail for.'), + model_name="ignoreemail", + name="queues", + field=models.ManyToManyField( + null=True, + to="helpdesk.Queue", + blank=True, + help_text="Leave blank for this e-mail to be ignored on all queues, or select those queues you wish to ignore this e-mail for.", + ), preserve_default=True, ), migrations.AddField( - model_name='followup', - name='ticket', - field=models.ForeignKey(verbose_name='Ticket', to='helpdesk.Ticket', on_delete=models.CASCADE), + model_name="followup", + name="ticket", + field=models.ForeignKey( + verbose_name="Ticket", to="helpdesk.Ticket", on_delete=models.CASCADE + ), preserve_default=True, ), migrations.AddField( - model_name='followup', - name='user', - field=models.ForeignKey(null=True, verbose_name='User', blank=True, to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + model_name="followup", + name="user", + field=models.ForeignKey( + null=True, + verbose_name="User", + blank=True, + to=settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ), preserve_default=True, ), migrations.AddField( - model_name='escalationexclusion', - name='queues', - field=models.ManyToManyField(null=True, to='helpdesk.Queue', blank=True, help_text='Leave blank for this exclusion to be applied to all queues, or select those queues you wish to exclude with this entry.'), + model_name="escalationexclusion", + name="queues", + field=models.ManyToManyField( + null=True, + to="helpdesk.Queue", + blank=True, + help_text="Leave blank for this exclusion to be applied to all queues, or select those queues you wish to exclude with this entry.", + ), preserve_default=True, ), migrations.AddField( - model_name='attachment', - name='followup', - field=models.ForeignKey(verbose_name='Follow-up', to='helpdesk.FollowUp', on_delete=models.CASCADE), + model_name="attachment", + name="followup", + field=models.ForeignKey( + verbose_name="Follow-up", + to="helpdesk.FollowUp", + on_delete=models.CASCADE, + ), preserve_default=True, ), ] diff --git a/helpdesk/migrations/0002_populate_usersettings.py b/helpdesk/migrations/0002_populate_usersettings.py index 42c94cf5..4dfd8d7e 100644 --- a/helpdesk/migrations/0002_populate_usersettings.py +++ b/helpdesk/migrations/0002_populate_usersettings.py @@ -12,6 +12,7 @@ def pickle_settings(data): except ImportError: import cPickle as pickle from helpdesk.query import b64encode + return b64encode(pickle.dumps(data)) @@ -38,14 +39,12 @@ def populate_usersettings(apps, schema_editor): noop = lambda *args, **kwargs: None -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ - ('helpdesk', '0001_initial'), + ("helpdesk", "0001_initial"), ] operations = [ migrations.RunPython(populate_usersettings, reverse_code=noop), ] - - diff --git a/helpdesk/migrations/0003_initial_data_import.py b/helpdesk/migrations/0003_initial_data_import.py index b3fcbb43..8c72d3af 100644 --- a/helpdesk/migrations/0003_initial_data_import.py +++ b/helpdesk/migrations/0003_initial_data_import.py @@ -4,14 +4,15 @@ import os from django.db import migrations from django.core import serializers -fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures')) -fixture_filename = 'emailtemplate.json' +fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../fixtures")) +fixture_filename = "emailtemplate.json" + def deserialize_fixture(): fixture_file = os.path.join(fixture_dir, fixture_filename) - with open(fixture_file, 'rb') as fixture: - return list(serializers.deserialize('json', fixture, ignorenonexistent=True)) + with open(fixture_file, "rb") as fixture: + return list(serializers.deserialize("json", fixture, ignorenonexistent=True)) def load_fixture(apps, schema_editor): @@ -27,13 +28,12 @@ def unload_fixture(apps, schema_editor): objects = deserialize_fixture() EmailTemplate = apps.get_model("helpdesk", "emailtemplate") - EmailTemplate.objects.filter(pk__in=[ obj.object.pk for obj in objects ]).delete() + EmailTemplate.objects.filter(pk__in=[obj.object.pk for obj in objects]).delete() class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0002_populate_usersettings'), + ("helpdesk", "0002_populate_usersettings"), ] operations = [ diff --git a/helpdesk/migrations/0004_add_per_queue_staff_membership.py b/helpdesk/migrations/0004_add_per_queue_staff_membership.py index d3a444f4..fa95e38c 100644 --- a/helpdesk/migrations/0004_add_per_queue_staff_membership.py +++ b/helpdesk/migrations/0004_add_per_queue_staff_membership.py @@ -4,23 +4,42 @@ from django.conf import settings class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('helpdesk', '0003_initial_data_import'), + ("helpdesk", "0003_initial_data_import"), ] operations = [ migrations.CreateModel( - name='QueueMembership', + name="QueueMembership", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('queues', models.ManyToManyField(to='helpdesk.Queue', verbose_name='Authorized Queues')), - ('user', models.OneToOneField(verbose_name='User', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "queues", + models.ManyToManyField( + to="helpdesk.Queue", verbose_name="Authorized Queues" + ), + ), + ( + "user", + models.OneToOneField( + verbose_name="User", + to=settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ), + ), ], options={ - 'verbose_name': 'Queue Membership', - 'verbose_name_plural': 'Queue Memberships', + "verbose_name": "Queue Membership", + "verbose_name_plural": "Queue Memberships", }, bases=(models.Model,), ), diff --git a/helpdesk/migrations/0005_queues_no_null.py b/helpdesk/migrations/0005_queues_no_null.py index e7e2ba76..b2c43e91 100644 --- a/helpdesk/migrations/0005_queues_no_null.py +++ b/helpdesk/migrations/0005_queues_no_null.py @@ -3,25 +3,36 @@ from django.db import models, migrations class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0004_add_per_queue_staff_membership'), + ("helpdesk", "0004_add_per_queue_staff_membership"), ] operations = [ migrations.AlterField( - model_name='escalationexclusion', - name='queues', - field=models.ManyToManyField(help_text='Leave blank for this exclusion to be applied to all queues, or select those queues you wish to exclude with this entry.', to='helpdesk.Queue', blank=True), + model_name="escalationexclusion", + name="queues", + field=models.ManyToManyField( + help_text="Leave blank for this exclusion to be applied to all queues, or select those queues you wish to exclude with this entry.", + to="helpdesk.Queue", + blank=True, + ), ), migrations.AlterField( - model_name='ignoreemail', - name='queues', - field=models.ManyToManyField(help_text='Leave blank for this e-mail to be ignored on all queues, or select those queues you wish to ignore this e-mail for.', to='helpdesk.Queue', blank=True), + model_name="ignoreemail", + name="queues", + field=models.ManyToManyField( + help_text="Leave blank for this e-mail to be ignored on all queues, or select those queues you wish to ignore this e-mail for.", + to="helpdesk.Queue", + blank=True, + ), ), migrations.AlterField( - model_name='presetreply', - name='queues', - field=models.ManyToManyField(help_text='Leave blank to allow this reply to be used for all queues, or select those queues you wish to limit this reply to.', to='helpdesk.Queue', blank=True), + model_name="presetreply", + name="queues", + field=models.ManyToManyField( + help_text="Leave blank to allow this reply to be used for all queues, or select those queues you wish to limit this reply to.", + to="helpdesk.Queue", + blank=True, + ), ), ] diff --git a/helpdesk/migrations/0006_email_maxlength.py b/helpdesk/migrations/0006_email_maxlength.py index e1e37927..b8eb001c 100644 --- a/helpdesk/migrations/0006_email_maxlength.py +++ b/helpdesk/migrations/0006_email_maxlength.py @@ -3,25 +3,42 @@ from django.db import models, migrations class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0005_queues_no_null'), + ("helpdesk", "0005_queues_no_null"), ] operations = [ migrations.AlterField( - model_name='queue', - name='email_address', - field=models.EmailField(help_text='All outgoing e-mails for this queue will use this e-mail address. If you use IMAP or POP3, this should be the e-mail address for that mailbox.', max_length=254, null=True, verbose_name='E-Mail Address', blank=True), + model_name="queue", + name="email_address", + field=models.EmailField( + help_text="All outgoing e-mails for this queue will use this e-mail address. If you use IMAP or POP3, this should be the e-mail address for that mailbox.", + max_length=254, + null=True, + verbose_name="E-Mail Address", + blank=True, + ), ), migrations.AlterField( - model_name='ticket', - name='submitter_email', - field=models.EmailField(help_text='The submitter will receive an email for all public follow-ups left for this task.', max_length=254, null=True, verbose_name='Submitter E-Mail', blank=True), + model_name="ticket", + name="submitter_email", + field=models.EmailField( + help_text="The submitter will receive an email for all public follow-ups left for this task.", + max_length=254, + null=True, + verbose_name="Submitter E-Mail", + blank=True, + ), ), migrations.AlterField( - model_name='ticketcc', - name='email', - field=models.EmailField(help_text='For non-user followers, enter their e-mail address', max_length=254, null=True, verbose_name='E-Mail Address', blank=True), + model_name="ticketcc", + name="email", + field=models.EmailField( + help_text="For non-user followers, enter their e-mail address", + max_length=254, + null=True, + verbose_name="E-Mail Address", + blank=True, + ), ), ] diff --git a/helpdesk/migrations/0007_max_length_by_integer.py b/helpdesk/migrations/0007_max_length_by_integer.py index 5b8deba3..daa19eb7 100644 --- a/helpdesk/migrations/0007_max_length_by_integer.py +++ b/helpdesk/migrations/0007_max_length_by_integer.py @@ -3,15 +3,18 @@ from django.db import models, migrations class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0006_email_maxlength'), + ("helpdesk", "0006_email_maxlength"), ] operations = [ migrations.AlterField( - model_name='customfield', - name='label', - field=models.CharField(help_text='The display label for this field', max_length=30, verbose_name='Label'), + model_name="customfield", + name="label", + field=models.CharField( + help_text="The display label for this field", + max_length=30, + verbose_name="Label", + ), ), ] diff --git a/helpdesk/migrations/0008_extra_for_permissions.py b/helpdesk/migrations/0008_extra_for_permissions.py index d9a75aa9..ec32c4f4 100644 --- a/helpdesk/migrations/0008_extra_for_permissions.py +++ b/helpdesk/migrations/0008_extra_for_permissions.py @@ -3,15 +3,20 @@ from django.db import models, migrations class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0007_max_length_by_integer'), + ("helpdesk", "0007_max_length_by_integer"), ] operations = [ migrations.AddField( - model_name='queue', - name='permission_name', - field=models.CharField(help_text='Name used in the django.contrib.auth permission system', max_length=50, null=True, verbose_name='Django auth permission name', blank=True), + model_name="queue", + name="permission_name", + field=models.CharField( + help_text="Name used in the django.contrib.auth permission system", + max_length=50, + null=True, + verbose_name="Django auth permission name", + blank=True, + ), ), ] diff --git a/helpdesk/migrations/0009_migrate_queuemembership.py b/helpdesk/migrations/0009_migrate_queuemembership.py index 0b88bdee..70ea24fa 100644 --- a/helpdesk/migrations/0009_migrate_queuemembership.py +++ b/helpdesk/migrations/0009_migrate_queuemembership.py @@ -7,14 +7,14 @@ from django.utils.translation import gettext_lazy as _ def create_and_assign_permissions(apps, schema_editor): db_alias = schema_editor.connection.alias - Permission = apps.get_model('auth', 'Permission') - ContentType = apps.get_model('contenttypes', 'ContentType') + Permission = apps.get_model("auth", "Permission") + ContentType = apps.get_model("contenttypes", "ContentType") # Two steps: # 1. Create the permission for existing Queues # 2. Assign the permission to user according to QueueMembership objects # First step: prepare the permission for each queue - Queue = apps.get_model('helpdesk', 'Queue') + Queue = apps.get_model("helpdesk", "Queue") for q in Queue.objects.using(db_alias).all(): if not q.permission_name: basename = "queue_access_%s" % q.slug @@ -35,7 +35,7 @@ def create_and_assign_permissions(apps, schema_editor): q.save() # Second step: map the permissions according to QueueMembership - QueueMembership = apps.get_model('helpdesk', 'QueueMembership') + QueueMembership = apps.get_model("helpdesk", "QueueMembership") for qm in QueueMembership.objects.using(db_alias).all(): user = qm.user for q in qm.queues.all(): @@ -47,9 +47,9 @@ def create_and_assign_permissions(apps, schema_editor): def revert_queue_membership(apps, schema_editor): db_alias = schema_editor.connection.alias - Permission = apps.get_model('auth', 'Permission') - Queue = apps.get_model('helpdesk', 'Queue') - QueueMembership = apps.get_model('helpdesk', 'QueueMembership') + Permission = apps.get_model("auth", "Permission") + Queue = apps.get_model("helpdesk", "Queue") + QueueMembership = apps.get_model("helpdesk", "QueueMembership") for p in Permission.objects.using(db_alias).all(): if p.codename.startswith("queue_access_"): slug = p.codename[13:] @@ -66,12 +66,10 @@ def revert_queue_membership(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0008_extra_for_permissions'), + ("helpdesk", "0008_extra_for_permissions"), ] operations = [ - migrations.RunPython(create_and_assign_permissions, - revert_queue_membership) + migrations.RunPython(create_and_assign_permissions, revert_queue_membership) ] diff --git a/helpdesk/migrations/0010_remove_queuemembership.py b/helpdesk/migrations/0010_remove_queuemembership.py index 98775daa..1b75ffce 100644 --- a/helpdesk/migrations/0010_remove_queuemembership.py +++ b/helpdesk/migrations/0010_remove_queuemembership.py @@ -3,21 +3,20 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0009_migrate_queuemembership'), + ("helpdesk", "0009_migrate_queuemembership"), ] operations = [ migrations.RemoveField( - model_name='queuemembership', - name='queues', + model_name="queuemembership", + name="queues", ), migrations.RemoveField( - model_name='queuemembership', - name='user', + model_name="queuemembership", + name="user", ), migrations.DeleteModel( - name='QueueMembership', + name="QueueMembership", ), ] diff --git a/helpdesk/migrations/0011_admin_related_improvements.py b/helpdesk/migrations/0011_admin_related_improvements.py index 1ac25317..080a0236 100644 --- a/helpdesk/migrations/0011_admin_related_improvements.py +++ b/helpdesk/migrations/0011_admin_related_improvements.py @@ -3,20 +3,30 @@ from django.db import models, migrations class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0010_remove_queuemembership'), + ("helpdesk", "0010_remove_queuemembership"), ] operations = [ migrations.AlterField( - model_name='queue', - name='permission_name', - field=models.CharField(editable=False, max_length=50, blank=True, help_text='Name used in the django.contrib.auth permission system', null=True, verbose_name='Django auth permission name'), + model_name="queue", + name="permission_name", + field=models.CharField( + editable=False, + max_length=50, + blank=True, + help_text="Name used in the django.contrib.auth permission system", + null=True, + verbose_name="Django auth permission name", + ), ), migrations.AlterField( - model_name='queue', - name='slug', - field=models.SlugField(help_text="This slug is used when building ticket ID's. Once set, try not to change it or e-mailing may get messy.", unique=True, verbose_name='Slug'), + model_name="queue", + name="slug", + field=models.SlugField( + help_text="This slug is used when building ticket ID's. Once set, try not to change it or e-mailing may get messy.", + unique=True, + verbose_name="Slug", + ), ), ] diff --git a/helpdesk/migrations/0012_queue_default_owner.py b/helpdesk/migrations/0012_queue_default_owner.py index aa8af6f4..73d4c4c0 100644 --- a/helpdesk/migrations/0012_queue_default_owner.py +++ b/helpdesk/migrations/0012_queue_default_owner.py @@ -6,16 +6,22 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('helpdesk', '0011_admin_related_improvements'), + ("helpdesk", "0011_admin_related_improvements"), ] operations = [ migrations.AddField( - model_name='queue', - name='default_owner', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='default_owner', to=settings.AUTH_USER_MODEL, verbose_name='Default owner'), + model_name="queue", + name="default_owner", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="default_owner", + to=settings.AUTH_USER_MODEL, + verbose_name="Default owner", + ), ), ] diff --git a/helpdesk/migrations/0013_email_box_local_dir_and_logging.py b/helpdesk/migrations/0013_email_box_local_dir_and_logging.py index 58e383e5..d2e0e2c8 100644 --- a/helpdesk/migrations/0013_email_box_local_dir_and_logging.py +++ b/helpdesk/migrations/0013_email_box_local_dir_and_logging.py @@ -4,30 +4,66 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0012_queue_default_owner'), + ("helpdesk", "0012_queue_default_owner"), ] operations = [ migrations.AddField( - model_name='queue', - name='email_box_local_dir', - field=models.CharField(blank=True, help_text='If using a local directory, what directory path do you wish to poll for new email? Example: /var/lib/mail/helpdesk/', max_length=200, null=True, verbose_name='E-Mail Local Directory'), + model_name="queue", + name="email_box_local_dir", + field=models.CharField( + blank=True, + help_text="If using a local directory, what directory path do you wish to poll for new email? Example: /var/lib/mail/helpdesk/", + max_length=200, + null=True, + verbose_name="E-Mail Local Directory", + ), ), migrations.AddField( - model_name='queue', - name='logging_dir', - field=models.CharField(blank=True, help_text='If logging is enabled, what directory should we use to store log files for this queue? The standard logging mechanims are used if no directory is set', max_length=200, null=True, verbose_name='Logging Directory'), + model_name="queue", + name="logging_dir", + field=models.CharField( + blank=True, + help_text="If logging is enabled, what directory should we use to store log files for this queue? The standard logging mechanims are used if no directory is set", + max_length=200, + null=True, + verbose_name="Logging Directory", + ), ), migrations.AddField( - model_name='queue', - name='logging_type', - field=models.CharField(blank=True, choices=[('none', 'None'), ('debug', 'Debug'), ('info', 'Information'), ('warn', 'Warning'), ('error', 'Error'), ('crit', 'Critical')], help_text='Set the default logging level. All messages at that level or above will be logged to the directory set below. If no level is set, logging will be disabled.', max_length=5, null=True, verbose_name='Logging Type'), + model_name="queue", + name="logging_type", + field=models.CharField( + blank=True, + choices=[ + ("none", "None"), + ("debug", "Debug"), + ("info", "Information"), + ("warn", "Warning"), + ("error", "Error"), + ("crit", "Critical"), + ], + help_text="Set the default logging level. All messages at that level or above will be logged to the directory set below. If no level is set, logging will be disabled.", + max_length=5, + null=True, + verbose_name="Logging Type", + ), ), migrations.AlterField( - model_name='queue', - name='email_box_type', - field=models.CharField(blank=True, choices=[('pop3', 'POP 3'), ('imap', 'IMAP'), ('local', 'Local Directory')], help_text='E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.', max_length=5, null=True, verbose_name='E-Mail Box Type'), + model_name="queue", + name="email_box_type", + field=models.CharField( + blank=True, + choices=[ + ("pop3", "POP 3"), + ("imap", "IMAP"), + ("local", "Local Directory"), + ], + help_text="E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.", + max_length=5, + null=True, + verbose_name="E-Mail Box Type", + ), ), ] diff --git a/helpdesk/migrations/0014_usersettings_related_name.py b/helpdesk/migrations/0014_usersettings_related_name.py index 857e0293..077cf159 100644 --- a/helpdesk/migrations/0014_usersettings_related_name.py +++ b/helpdesk/migrations/0014_usersettings_related_name.py @@ -4,17 +4,18 @@ from django.conf import settings class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0013_email_box_local_dir_and_logging'), + ("helpdesk", "0013_email_box_local_dir_and_logging"), ] operations = [ migrations.AlterField( - model_name='usersettings', - name='user', - field=models.OneToOneField(to=settings.AUTH_USER_MODEL, - related_name='usersettings_helpdesk', - on_delete=models.CASCADE), + model_name="usersettings", + name="user", + field=models.OneToOneField( + to=settings.AUTH_USER_MODEL, + related_name="usersettings_helpdesk", + on_delete=models.CASCADE, + ), ), ] diff --git a/helpdesk/migrations/0015_expand_permission_name_size.py b/helpdesk/migrations/0015_expand_permission_name_size.py index b5aa3a1d..10a089da 100644 --- a/helpdesk/migrations/0015_expand_permission_name_size.py +++ b/helpdesk/migrations/0015_expand_permission_name_size.py @@ -4,15 +4,21 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0014_usersettings_related_name'), + ("helpdesk", "0014_usersettings_related_name"), ] operations = [ migrations.AlterField( - model_name='queue', - name='permission_name', - field=models.CharField(blank=True, editable=False, help_text='Name used in the django.contrib.auth permission system', max_length=72, null=True, verbose_name='Django auth permission name'), + model_name="queue", + name="permission_name", + field=models.CharField( + blank=True, + editable=False, + help_text="Name used in the django.contrib.auth permission system", + max_length=72, + null=True, + verbose_name="Django auth permission name", + ), ), ] diff --git a/helpdesk/migrations/0016_alter_model_options.py b/helpdesk/migrations/0016_alter_model_options.py index 426b5864..e7a2990c 100644 --- a/helpdesk/migrations/0016_alter_model_options.py +++ b/helpdesk/migrations/0016_alter_model_options.py @@ -4,38 +4,61 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0015_expand_permission_name_size'), + ("helpdesk", "0015_expand_permission_name_size"), ] operations = [ migrations.AlterModelOptions( - name='attachment', - options={'ordering': ('filename',), 'verbose_name': 'Attachment', 'verbose_name_plural': 'Attachments'}, + name="attachment", + options={ + "ordering": ("filename",), + "verbose_name": "Attachment", + "verbose_name_plural": "Attachments", + }, ), migrations.AlterModelOptions( - name='emailtemplate', - options={'ordering': ('template_name', 'locale'), 'verbose_name': 'e-mail template', 'verbose_name_plural': 'e-mail templates'}, + name="emailtemplate", + options={ + "ordering": ("template_name", "locale"), + "verbose_name": "e-mail template", + "verbose_name_plural": "e-mail templates", + }, ), migrations.AlterModelOptions( - name='followup', - options={'ordering': ('date',), 'verbose_name': 'Follow-up', 'verbose_name_plural': 'Follow-ups'}, + name="followup", + options={ + "ordering": ("date",), + "verbose_name": "Follow-up", + "verbose_name_plural": "Follow-ups", + }, ), migrations.AlterModelOptions( - name='kbcategory', - options={'ordering': ('title',), 'verbose_name': 'Knowledge base category', 'verbose_name_plural': 'Knowledge base categories'}, + name="kbcategory", + options={ + "ordering": ("title",), + "verbose_name": "Knowledge base category", + "verbose_name_plural": "Knowledge base categories", + }, ), migrations.AlterModelOptions( - name='kbitem', - options={'ordering': ('title',), 'verbose_name': 'Knowledge base item', 'verbose_name_plural': 'Knowledge base items'}, + name="kbitem", + options={ + "ordering": ("title",), + "verbose_name": "Knowledge base item", + "verbose_name_plural": "Knowledge base items", + }, ), migrations.AlterModelOptions( - name='presetreply', - options={'ordering': ('name',), 'verbose_name': 'Pre-set reply', 'verbose_name_plural': 'Pre-set replies'}, + name="presetreply", + options={ + "ordering": ("name",), + "verbose_name": "Pre-set reply", + "verbose_name_plural": "Pre-set replies", + }, ), migrations.AlterUniqueTogether( - name='ticketcustomfieldvalue', - unique_together=set([('ticket', 'field')]), + name="ticketcustomfieldvalue", + unique_together=set([("ticket", "field")]), ), ] diff --git a/helpdesk/migrations/0017_default_owner_on_delete_null.py b/helpdesk/migrations/0017_default_owner_on_delete_null.py index ee8cad36..526b4250 100644 --- a/helpdesk/migrations/0017_default_owner_on_delete_null.py +++ b/helpdesk/migrations/0017_default_owner_on_delete_null.py @@ -6,15 +6,21 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0016_alter_model_options'), + ("helpdesk", "0016_alter_model_options"), ] operations = [ migrations.AlterField( - model_name='queue', - name='default_owner', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_owner', to=settings.AUTH_USER_MODEL, verbose_name='Default owner'), + model_name="queue", + name="default_owner", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="default_owner", + to=settings.AUTH_USER_MODEL, + verbose_name="Default owner", + ), ), ] diff --git a/helpdesk/migrations/0018_ticket_secret_key.py b/helpdesk/migrations/0018_ticket_secret_key.py index 05ee0011..7dd0121b 100644 --- a/helpdesk/migrations/0018_ticket_secret_key.py +++ b/helpdesk/migrations/0018_ticket_secret_key.py @@ -4,55 +4,99 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0017_default_owner_on_delete_null'), + ("helpdesk", "0017_default_owner_on_delete_null"), ] operations = [ migrations.AlterField( - model_name='followup', - name='public', - field=models.BooleanField(blank=True, default=False, help_text='Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.', verbose_name='Public'), + model_name="followup", + name="public", + field=models.BooleanField( + blank=True, + default=False, + help_text="Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.", + verbose_name="Public", + ), ), migrations.AlterField( - model_name='ignoreemail', - name='keep_in_mailbox', - field=models.BooleanField(blank=True, default=False, help_text='Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.', verbose_name='Save Emails in Mailbox?'), + model_name="ignoreemail", + name="keep_in_mailbox", + field=models.BooleanField( + blank=True, + default=False, + help_text="Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.", + verbose_name="Save Emails in Mailbox?", + ), ), migrations.AlterField( - model_name='queue', - name='allow_email_submission', - field=models.BooleanField(blank=True, default=False, help_text='Do you want to poll the e-mail box below for new tickets?', verbose_name='Allow E-Mail Submission?'), + model_name="queue", + name="allow_email_submission", + field=models.BooleanField( + blank=True, + default=False, + help_text="Do you want to poll the e-mail box below for new tickets?", + verbose_name="Allow E-Mail Submission?", + ), ), migrations.AlterField( - model_name='queue', - name='allow_public_submission', - field=models.BooleanField(blank=True, default=False, help_text='Should this queue be listed on the public submission form?', verbose_name='Allow Public Submission?'), + model_name="queue", + name="allow_public_submission", + field=models.BooleanField( + blank=True, + default=False, + help_text="Should this queue be listed on the public submission form?", + verbose_name="Allow Public Submission?", + ), ), migrations.AlterField( - model_name='queue', - name='email_box_ssl', - field=models.BooleanField(blank=True, default=False, help_text='Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.', verbose_name='Use SSL for E-Mail?'), + model_name="queue", + name="email_box_ssl", + field=models.BooleanField( + blank=True, + default=False, + help_text="Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.", + verbose_name="Use SSL for E-Mail?", + ), ), migrations.AlterField( - model_name='savedsearch', - name='shared', - field=models.BooleanField(blank=True, default=False, help_text='Should other users see this query?', verbose_name='Shared With Other Users?'), + model_name="savedsearch", + name="shared", + field=models.BooleanField( + blank=True, + default=False, + help_text="Should other users see this query?", + verbose_name="Shared With Other Users?", + ), ), migrations.AlterField( - model_name='ticket', - name='on_hold', - field=models.BooleanField(blank=True, default=False, help_text='If a ticket is on hold, it will not automatically be escalated.', verbose_name='On Hold'), + model_name="ticket", + name="on_hold", + field=models.BooleanField( + blank=True, + default=False, + help_text="If a ticket is on hold, it will not automatically be escalated.", + verbose_name="On Hold", + ), ), migrations.AlterField( - model_name='ticketcc', - name='can_update', - field=models.BooleanField(blank=True, default=False, help_text='Can this CC login and update the ticket?', verbose_name='Can Update Ticket?'), + model_name="ticketcc", + name="can_update", + field=models.BooleanField( + blank=True, + default=False, + help_text="Can this CC login and update the ticket?", + verbose_name="Can Update Ticket?", + ), ), migrations.AlterField( - model_name='ticketcc', - name='can_view', - field=models.BooleanField(blank=True, default=False, help_text='Can this CC login to view the ticket details?', verbose_name='Can View Ticket?'), + model_name="ticketcc", + name="can_view", + field=models.BooleanField( + blank=True, + default=False, + help_text="Can this CC login to view the ticket details?", + verbose_name="Can View Ticket?", + ), ), ] diff --git a/helpdesk/migrations/0019_ticket_secret_key.py b/helpdesk/migrations/0019_ticket_secret_key.py index 47f10571..5e59de9a 100644 --- a/helpdesk/migrations/0019_ticket_secret_key.py +++ b/helpdesk/migrations/0019_ticket_secret_key.py @@ -8,22 +8,24 @@ def clear_secret_keys(apps, schema_editor): db_alias = schema_editor.connection.alias for ticket in Ticket.objects.using(db_alias).all(): - ticket.secret_key = '' + ticket.secret_key = "" ticket.save() class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0018_ticket_secret_key'), + ("helpdesk", "0018_ticket_secret_key"), ] operations = [ migrations.AddField( - model_name='ticket', - name='secret_key', - field=models.CharField(default=helpdesk.models.mk_secret, max_length=36, - verbose_name='Secret key needed for viewing/editing ticket by non-logged in users'), + model_name="ticket", + name="secret_key", + field=models.CharField( + default=helpdesk.models.mk_secret, + max_length=36, + verbose_name="Secret key needed for viewing/editing ticket by non-logged in users", + ), ), migrations.RunPython(clear_secret_keys), ] diff --git a/helpdesk/migrations/0020_depickle_user_settings.py b/helpdesk/migrations/0020_depickle_user_settings.py index bf077b95..3b31fd3f 100644 --- a/helpdesk/migrations/0020_depickle_user_settings.py +++ b/helpdesk/migrations/0020_depickle_user_settings.py @@ -16,7 +16,7 @@ def unpickle_settings(settings_pickled): # Python 3 support from base64 import decodebytes as b64decode try: - return pickle.loads(b64decode(settings_pickled.encode('utf-8'))) + return pickle.loads(b64decode(settings_pickled.encode("utf-8"))) except Exception: return {} @@ -33,41 +33,66 @@ def move_old_values(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0019_ticket_secret_key'), + ("helpdesk", "0019_ticket_secret_key"), ] operations = [ migrations.AddField( - model_name='usersettings', - name='email_on_ticket_assign', - field=models.BooleanField(default=helpdesk.models.email_on_ticket_assign_default, help_text='If you are assigned a ticket via the web, do you want to receive an e-mail?', verbose_name='E-mail me when assigned a ticket?'), + model_name="usersettings", + name="email_on_ticket_assign", + field=models.BooleanField( + default=helpdesk.models.email_on_ticket_assign_default, + help_text="If you are assigned a ticket via the web, do you want to receive an e-mail?", + verbose_name="E-mail me when assigned a ticket?", + ), ), migrations.AddField( - model_name='usersettings', - name='email_on_ticket_change', - field=models.BooleanField(default=helpdesk.models.email_on_ticket_change_default, help_text="If you're the ticket owner and the ticket is changed via the web by somebody else, do you want to receive an e-mail?", verbose_name='E-mail me on ticket change?'), + model_name="usersettings", + name="email_on_ticket_change", + field=models.BooleanField( + default=helpdesk.models.email_on_ticket_change_default, + help_text="If you're the ticket owner and the ticket is changed via the web by somebody else, do you want to receive an e-mail?", + verbose_name="E-mail me on ticket change?", + ), ), migrations.AddField( - model_name='usersettings', - name='login_view_ticketlist', - field=models.BooleanField(default=helpdesk.models.login_view_ticketlist_default, help_text='Display the ticket list upon login? Otherwise, the dashboard is shown.', verbose_name='Show Ticket List on Login?'), + model_name="usersettings", + name="login_view_ticketlist", + field=models.BooleanField( + default=helpdesk.models.login_view_ticketlist_default, + help_text="Display the ticket list upon login? Otherwise, the dashboard is shown.", + verbose_name="Show Ticket List on Login?", + ), ), migrations.AddField( - model_name='usersettings', - name='tickets_per_page', - field=models.IntegerField(choices=[(10, '10'), (25, '25'), (50, '50'), (100, '100')], default=helpdesk.models.tickets_per_page_default, help_text='How many tickets do you want to see on the Ticket List page?', verbose_name='Number of tickets to show per page'), + model_name="usersettings", + name="tickets_per_page", + field=models.IntegerField( + choices=[(10, "10"), (25, "25"), (50, "50"), (100, "100")], + default=helpdesk.models.tickets_per_page_default, + help_text="How many tickets do you want to see on the Ticket List page?", + verbose_name="Number of tickets to show per page", + ), ), migrations.AddField( - model_name='usersettings', - name='use_email_as_submitter', - field=models.BooleanField(default=helpdesk.models.use_email_as_submitter_default, help_text='When you submit a ticket, do you want to automatically use your e-mail address as the submitter address? You can type a different e-mail address when entering the ticket if needed, this option only changes the default.', verbose_name='Use my e-mail address when submitting tickets?'), + model_name="usersettings", + name="use_email_as_submitter", + field=models.BooleanField( + default=helpdesk.models.use_email_as_submitter_default, + help_text="When you submit a ticket, do you want to automatically use your e-mail address as the submitter address? You can type a different e-mail address when entering the ticket if needed, this option only changes the default.", + verbose_name="Use my e-mail address when submitting tickets?", + ), ), migrations.AlterField( - model_name='usersettings', - name='settings_pickled', - field=models.TextField(blank=True, help_text='DEPRECATED! This is a base64-encoded representation of a pickled Python dictionary. Do not change this field via the admin.', null=True, verbose_name='DEPRECATED! Settings Dictionary DEPRECATED!'), + model_name="usersettings", + name="settings_pickled", + field=models.TextField( + blank=True, + help_text="DEPRECATED! This is a base64-encoded representation of a pickled Python dictionary. Do not change this field via the admin.", + null=True, + verbose_name="DEPRECATED! Settings Dictionary DEPRECATED!", + ), ), migrations.RunPython(move_old_values), ] diff --git a/helpdesk/migrations/0021_voting_tracker.py b/helpdesk/migrations/0021_voting_tracker.py index 691977a2..63a3177c 100644 --- a/helpdesk/migrations/0021_voting_tracker.py +++ b/helpdesk/migrations/0021_voting_tracker.py @@ -4,61 +4,105 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('helpdesk', '0020_depickle_user_settings'), + ("helpdesk", "0020_depickle_user_settings"), ] operations = [ migrations.AddField( - model_name='kbitem', - name='voted_by', + model_name="kbitem", + name="voted_by", field=models.ManyToManyField(to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name='followup', - name='public', - field=models.BooleanField(blank=True, default=False, help_text='Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.', verbose_name='Public'), + model_name="followup", + name="public", + field=models.BooleanField( + blank=True, + default=False, + help_text="Public tickets are viewable by the submitter and all staff, but non-public tickets can only be seen by staff.", + verbose_name="Public", + ), ), migrations.AlterField( - model_name='ignoreemail', - name='keep_in_mailbox', - field=models.BooleanField(blank=True, default=False, help_text='Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.', verbose_name='Save Emails in Mailbox?'), + model_name="ignoreemail", + name="keep_in_mailbox", + field=models.BooleanField( + blank=True, + default=False, + help_text="Do you want to save emails from this address in the mailbox? If this is unticked, emails from this address will be deleted.", + verbose_name="Save Emails in Mailbox?", + ), ), migrations.AlterField( - model_name='queue', - name='allow_email_submission', - field=models.BooleanField(blank=True, default=False, help_text='Do you want to poll the e-mail box below for new tickets?', verbose_name='Allow E-Mail Submission?'), + model_name="queue", + name="allow_email_submission", + field=models.BooleanField( + blank=True, + default=False, + help_text="Do you want to poll the e-mail box below for new tickets?", + verbose_name="Allow E-Mail Submission?", + ), ), migrations.AlterField( - model_name='queue', - name='allow_public_submission', - field=models.BooleanField(blank=True, default=False, help_text='Should this queue be listed on the public submission form?', verbose_name='Allow Public Submission?'), + model_name="queue", + name="allow_public_submission", + field=models.BooleanField( + blank=True, + default=False, + help_text="Should this queue be listed on the public submission form?", + verbose_name="Allow Public Submission?", + ), ), migrations.AlterField( - model_name='queue', - name='email_box_ssl', - field=models.BooleanField(blank=True, default=False, help_text='Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.', verbose_name='Use SSL for E-Mail?'), + model_name="queue", + name="email_box_ssl", + field=models.BooleanField( + blank=True, + default=False, + help_text="Whether to use SSL for IMAP or POP3 - the default ports when using SSL are 993 for IMAP and 995 for POP3.", + verbose_name="Use SSL for E-Mail?", + ), ), migrations.AlterField( - model_name='savedsearch', - name='shared', - field=models.BooleanField(blank=True, default=False, help_text='Should other users see this query?', verbose_name='Shared With Other Users?'), + model_name="savedsearch", + name="shared", + field=models.BooleanField( + blank=True, + default=False, + help_text="Should other users see this query?", + verbose_name="Shared With Other Users?", + ), ), migrations.AlterField( - model_name='ticket', - name='on_hold', - field=models.BooleanField(blank=True, default=False, help_text='If a ticket is on hold, it will not automatically be escalated.', verbose_name='On Hold'), + model_name="ticket", + name="on_hold", + field=models.BooleanField( + blank=True, + default=False, + help_text="If a ticket is on hold, it will not automatically be escalated.", + verbose_name="On Hold", + ), ), migrations.AlterField( - model_name='ticketcc', - name='can_update', - field=models.BooleanField(blank=True, default=False, help_text='Can this CC login and update the ticket?', verbose_name='Can Update Ticket?'), + model_name="ticketcc", + name="can_update", + field=models.BooleanField( + blank=True, + default=False, + help_text="Can this CC login and update the ticket?", + verbose_name="Can Update Ticket?", + ), ), migrations.AlterField( - model_name='ticketcc', - name='can_view', - field=models.BooleanField(blank=True, default=False, help_text='Can this CC login to view the ticket details?', verbose_name='Can View Ticket?'), + model_name="ticketcc", + name="can_view", + field=models.BooleanField( + blank=True, + default=False, + help_text="Can this CC login to view the ticket details?", + verbose_name="Can View Ticket?", + ), ), ] diff --git a/helpdesk/migrations/0022_add_submitter_email_id_field_to_ticket.py b/helpdesk/migrations/0022_add_submitter_email_id_field_to_ticket.py index 614204b2..6365f93a 100644 --- a/helpdesk/migrations/0022_add_submitter_email_id_field_to_ticket.py +++ b/helpdesk/migrations/0022_add_submitter_email_id_field_to_ticket.py @@ -4,15 +4,21 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0021_voting_tracker'), + ("helpdesk", "0021_voting_tracker"), ] operations = [ migrations.AddField( - model_name='followup', - name='message_id', - field=models.CharField(blank=True, editable=False, help_text="The Message ID of the submitter's email.", max_length=256, null=True, verbose_name='E-Mail ID'), + model_name="followup", + name="message_id", + field=models.CharField( + blank=True, + editable=False, + help_text="The Message ID of the submitter's email.", + max_length=256, + null=True, + verbose_name="E-Mail ID", + ), ), ] diff --git a/helpdesk/migrations/0023_add_enable_notifications_on_email_events_to_ticket.py b/helpdesk/migrations/0023_add_enable_notifications_on_email_events_to_ticket.py index 18a10d77..8f11fe0c 100644 --- a/helpdesk/migrations/0023_add_enable_notifications_on_email_events_to_ticket.py +++ b/helpdesk/migrations/0023_add_enable_notifications_on_email_events_to_ticket.py @@ -4,15 +4,18 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0022_add_submitter_email_id_field_to_ticket'), + ("helpdesk", "0022_add_submitter_email_id_field_to_ticket"), ] operations = [ migrations.AddField( - model_name='queue', - name='enable_notifications_on_email_events', - field=models.BooleanField(default=False, help_text='When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature', verbose_name='Notify contacts when email updates arrive'), + model_name="queue", + name="enable_notifications_on_email_events", + field=models.BooleanField( + default=False, + help_text="When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature", + verbose_name="Notify contacts when email updates arrive", + ), ), ] diff --git a/helpdesk/migrations/0024_time_spent.py b/helpdesk/migrations/0024_time_spent.py index bbb0f22f..ebb8ef6a 100644 --- a/helpdesk/migrations/0024_time_spent.py +++ b/helpdesk/migrations/0024_time_spent.py @@ -4,15 +4,16 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0023_add_enable_notifications_on_email_events_to_ticket'), + ("helpdesk", "0023_add_enable_notifications_on_email_events_to_ticket"), ] operations = [ migrations.AddField( - model_name='followup', - name='time_spent', - field=models.DurationField(blank=True, help_text='Time spent on this follow up', null=True), + model_name="followup", + name="time_spent", + field=models.DurationField( + blank=True, help_text="Time spent on this follow up", null=True + ), ), ] diff --git a/helpdesk/migrations/0025_queue_dedicated_time.py b/helpdesk/migrations/0025_queue_dedicated_time.py index d3dfd8d3..d71169ef 100644 --- a/helpdesk/migrations/0025_queue_dedicated_time.py +++ b/helpdesk/migrations/0025_queue_dedicated_time.py @@ -4,15 +4,18 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0024_time_spent'), + ("helpdesk", "0024_time_spent"), ] operations = [ migrations.AddField( - model_name='queue', - name='dedicated_time', - field=models.DurationField(blank=True, help_text='Time to be spent on this Queue in total', null=True), + model_name="queue", + name="dedicated_time", + field=models.DurationField( + blank=True, + help_text="Time to be spent on this Queue in total", + null=True, + ), ), ] diff --git a/helpdesk/migrations/0026_kbitem_attachments.py b/helpdesk/migrations/0026_kbitem_attachments.py index 810672c5..6a0fa1af 100644 --- a/helpdesk/migrations/0026_kbitem_attachments.py +++ b/helpdesk/migrations/0026_kbitem_attachments.py @@ -6,31 +6,63 @@ import helpdesk.models class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0025_queue_dedicated_time'), + ("helpdesk", "0025_queue_dedicated_time"), ] operations = [ migrations.CreateModel( - name='KBIAttachment', + name="KBIAttachment", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('file', models.FileField(max_length=1000, upload_to=helpdesk.models.attachment_path, verbose_name='File')), - ('filename', models.CharField(max_length=1000, verbose_name='Filename')), - ('mime_type', models.CharField(max_length=255, verbose_name='MIME Type')), - ('size', models.IntegerField(help_text='Size of this file in bytes', verbose_name='Size')), - ('kbitem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='helpdesk.KBItem', verbose_name='Knowledge base item')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "file", + models.FileField( + max_length=1000, + upload_to=helpdesk.models.attachment_path, + verbose_name="File", + ), + ), + ( + "filename", + models.CharField(max_length=1000, verbose_name="Filename"), + ), + ( + "mime_type", + models.CharField(max_length=255, verbose_name="MIME Type"), + ), + ( + "size", + models.IntegerField( + help_text="Size of this file in bytes", verbose_name="Size" + ), + ), + ( + "kbitem", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="helpdesk.KBItem", + verbose_name="Knowledge base item", + ), + ), ], options={ - 'verbose_name': 'Attachment', - 'verbose_name_plural': 'Attachments', - 'ordering': ('filename',), - 'abstract': False, + "verbose_name": "Attachment", + "verbose_name_plural": "Attachments", + "ordering": ("filename",), + "abstract": False, }, ), migrations.RenameModel( - old_name='Attachment', - new_name='FollowUpAttachment', + old_name="Attachment", + new_name="FollowUpAttachment", ), ] diff --git a/helpdesk/migrations/0027_auto_20200107_1221.py b/helpdesk/migrations/0027_auto_20200107_1221.py index dbd50149..d34d1f4d 100644 --- a/helpdesk/migrations/0027_auto_20200107_1221.py +++ b/helpdesk/migrations/0027_auto_20200107_1221.py @@ -6,66 +6,98 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('helpdesk', '0026_kbitem_attachments'), + ("helpdesk", "0026_kbitem_attachments"), ] operations = [ migrations.AddField( - model_name='kbcategory', - name='queue', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='helpdesk.Queue', verbose_name='Default queue when creating a ticket after viewing this category.'), + model_name="kbcategory", + name="queue", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="helpdesk.Queue", + verbose_name="Default queue when creating a ticket after viewing this category.", + ), ), migrations.AddField( - model_name='kbitem', - name='downvoted_by', - field=models.ManyToManyField(related_name='downvotes', to=settings.AUTH_USER_MODEL), + model_name="kbitem", + name="downvoted_by", + field=models.ManyToManyField( + related_name="downvotes", to=settings.AUTH_USER_MODEL + ), ), migrations.AddField( - model_name='ticket', - name='kbitem', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='helpdesk.KBItem', verbose_name='Knowledge base item the user was viewing when they created this ticket.'), + model_name="ticket", + name="kbitem", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="helpdesk.KBItem", + verbose_name="Knowledge base item the user was viewing when they created this ticket.", + ), ), migrations.AlterField( - model_name='followupattachment', - name='filename', - field=models.CharField(blank=True, max_length=1000, verbose_name='Filename'), + model_name="followupattachment", + name="filename", + field=models.CharField( + blank=True, max_length=1000, verbose_name="Filename" + ), ), migrations.AlterField( - model_name='followupattachment', - name='mime_type', - field=models.CharField(blank=True, max_length=255, verbose_name='MIME Type'), + model_name="followupattachment", + name="mime_type", + field=models.CharField( + blank=True, max_length=255, verbose_name="MIME Type" + ), ), migrations.AlterField( - model_name='followupattachment', - name='size', - field=models.IntegerField(blank=True, help_text='Size of this file in bytes', verbose_name='Size'), + model_name="followupattachment", + name="size", + field=models.IntegerField( + blank=True, help_text="Size of this file in bytes", verbose_name="Size" + ), ), migrations.AlterField( - model_name='kbiattachment', - name='filename', - field=models.CharField(blank=True, max_length=1000, verbose_name='Filename'), + model_name="kbiattachment", + name="filename", + field=models.CharField( + blank=True, max_length=1000, verbose_name="Filename" + ), ), migrations.AlterField( - model_name='kbiattachment', - name='mime_type', - field=models.CharField(blank=True, max_length=255, verbose_name='MIME Type'), + model_name="kbiattachment", + name="mime_type", + field=models.CharField( + blank=True, max_length=255, verbose_name="MIME Type" + ), ), migrations.AlterField( - model_name='kbiattachment', - name='size', - field=models.IntegerField(blank=True, help_text='Size of this file in bytes', verbose_name='Size'), + model_name="kbiattachment", + name="size", + field=models.IntegerField( + blank=True, help_text="Size of this file in bytes", verbose_name="Size" + ), ), migrations.AlterField( - model_name='kbitem', - name='voted_by', - field=models.ManyToManyField(related_name='votes', to=settings.AUTH_USER_MODEL), + model_name="kbitem", + name="voted_by", + field=models.ManyToManyField( + related_name="votes", to=settings.AUTH_USER_MODEL + ), ), migrations.AlterField( - model_name='queue', - name='enable_notifications_on_email_events', - field=models.BooleanField(blank=True, default=False, help_text='When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature', verbose_name='Notify contacts when email updates arrive'), + model_name="queue", + name="enable_notifications_on_email_events", + field=models.BooleanField( + blank=True, + default=False, + help_text="When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature", + verbose_name="Notify contacts when email updates arrive", + ), ), ] diff --git a/helpdesk/migrations/0028_kbitem_team.py b/helpdesk/migrations/0028_kbitem_team.py index c0bdaf5d..5a8a284b 100644 --- a/helpdesk/migrations/0028_kbitem_team.py +++ b/helpdesk/migrations/0028_kbitem_team.py @@ -7,15 +7,20 @@ from helpdesk import settings as helpdesk_settings class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0027_auto_20200107_1221'), + ("helpdesk", "0027_auto_20200107_1221"), ] + helpdesk_settings.HELPDESK_TEAMS_MIGRATION_DEPENDENCIES operations = [ migrations.AddField( - model_name='kbitem', - name='team', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=helpdesk_settings.HELPDESK_TEAMS_MODEL, verbose_name='Team'), + model_name="kbitem", + name="team", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=helpdesk_settings.HELPDESK_TEAMS_MODEL, + verbose_name="Team", + ), ), ] diff --git a/helpdesk/migrations/0029_kbcategory_public.py b/helpdesk/migrations/0029_kbcategory_public.py index 1cca37be..3ca2d9b7 100644 --- a/helpdesk/migrations/0029_kbcategory_public.py +++ b/helpdesk/migrations/0029_kbcategory_public.py @@ -4,15 +4,16 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0028_kbitem_team'), + ("helpdesk", "0028_kbitem_team"), ] operations = [ migrations.AddField( - model_name='kbcategory', - name='public', - field=models.BooleanField(default=True, verbose_name='Is KBCategory publicly visible?'), + model_name="kbcategory", + name="public", + field=models.BooleanField( + default=True, verbose_name="Is KBCategory publicly visible?" + ), ), ] diff --git a/helpdesk/migrations/0030_add_kbcategory_name.py b/helpdesk/migrations/0030_add_kbcategory_name.py index 908fdf17..6fab1658 100644 --- a/helpdesk/migrations/0030_add_kbcategory_name.py +++ b/helpdesk/migrations/0030_add_kbcategory_name.py @@ -2,32 +2,44 @@ from django.db import migrations, models + def copy_title(apps, schema_editor): KBCategory = apps.get_model("helpdesk", "KBCategory") - KBCategory.objects.update(name=models.F('title')) + KBCategory.objects.update(name=models.F("title")) class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0029_kbcategory_public'), + ("helpdesk", "0029_kbcategory_public"), ] operations = [ migrations.AddField( - model_name='kbcategory', - name='name', - field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Name of the category'), + model_name="kbcategory", + name="name", + field=models.CharField( + blank=True, + max_length=100, + null=True, + verbose_name="Name of the category", + ), ), migrations.AlterField( - model_name='kbcategory', - name='title', - field=models.CharField(max_length=100, verbose_name='Title on knowledgebase page'), + model_name="kbcategory", + name="title", + field=models.CharField( + max_length=100, verbose_name="Title on knowledgebase page" + ), ), migrations.RunPython(copy_title, migrations.RunPython.noop), migrations.AlterField( - model_name='kbcategory', - name='name', - field=models.CharField(blank=False, max_length=100, null=False, verbose_name='Name of the category'), + model_name="kbcategory", + name="name", + field=models.CharField( + blank=False, + max_length=100, + null=False, + verbose_name="Name of the category", + ), ), ] diff --git a/helpdesk/migrations/0031_auto_20200225_1440.py b/helpdesk/migrations/0031_auto_20200225_1440.py index c287f06f..678919ce 100644 --- a/helpdesk/migrations/0031_auto_20200225_1440.py +++ b/helpdesk/migrations/0031_auto_20200225_1440.py @@ -4,19 +4,24 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0030_add_kbcategory_name'), + ("helpdesk", "0030_add_kbcategory_name"), ] operations = [ migrations.AlterModelOptions( - name='kbitem', - options={'ordering': ('order', 'title'), 'verbose_name': 'Knowledge base item', 'verbose_name_plural': 'Knowledge base items'}, + name="kbitem", + options={ + "ordering": ("order", "title"), + "verbose_name": "Knowledge base item", + "verbose_name_plural": "Knowledge base items", + }, ), migrations.AddField( - model_name='kbitem', - name='order', - field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Order'), + model_name="kbitem", + name="order", + field=models.PositiveIntegerField( + blank=True, null=True, verbose_name="Order" + ), ), ] diff --git a/helpdesk/migrations/0032_kbitem_enabled.py b/helpdesk/migrations/0032_kbitem_enabled.py index 7868bfe1..ffe2ae0a 100644 --- a/helpdesk/migrations/0032_kbitem_enabled.py +++ b/helpdesk/migrations/0032_kbitem_enabled.py @@ -4,15 +4,16 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0031_auto_20200225_1440'), + ("helpdesk", "0031_auto_20200225_1440"), ] operations = [ migrations.AddField( - model_name='kbitem', - name='enabled', - field=models.BooleanField(default=True, verbose_name='Enabled to display to users'), + model_name="kbitem", + name="enabled", + field=models.BooleanField( + default=True, verbose_name="Enabled to display to users" + ), ), ] diff --git a/helpdesk/migrations/0033_ticket_merged_to.py b/helpdesk/migrations/0033_ticket_merged_to.py index 98899213..a05cc657 100644 --- a/helpdesk/migrations/0033_ticket_merged_to.py +++ b/helpdesk/migrations/0033_ticket_merged_to.py @@ -5,15 +5,21 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0032_kbitem_enabled'), + ("helpdesk", "0032_kbitem_enabled"), ] operations = [ migrations.AddField( - model_name='ticket', - name='merged_to', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='merged_tickets', to='helpdesk.Ticket', verbose_name='merged to'), + model_name="ticket", + name="merged_to", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="merged_tickets", + to="helpdesk.Ticket", + verbose_name="merged to", + ), ), ] diff --git a/helpdesk/migrations/0034_create_email_template_for_merged.py b/helpdesk/migrations/0034_create_email_template_for_merged.py index 018faf57..446549d6 100644 --- a/helpdesk/migrations/0034_create_email_template_for_merged.py +++ b/helpdesk/migrations/0034_create_email_template_for_merged.py @@ -7,10 +7,12 @@ def forwards_func(apps, schema_editor): EmailTemplate = apps.get_model("helpdesk", "EmailTemplate") db_alias = schema_editor.connection.alias EmailTemplate.objects.using(db_alias).create( - id=EmailTemplate.objects.using(db_alias).order_by('-id').first().id + 1 if EmailTemplate.objects.using(db_alias).first() else 1, # because PG sequences are not reset - template_name='merged', - subject='(Merged)', - heading='Ticket merged', + id=EmailTemplate.objects.using(db_alias).order_by("-id").first().id + 1 + if EmailTemplate.objects.using(db_alias).first() + else 1, # because PG sequences are not reset + template_name="merged", + subject="(Merged)", + heading="Ticket merged", plain_text="""Hello, This is a courtesy e-mail to let you know that ticket {{ ticket.ticket }} ("{{ ticket.title }}") by {{ ticket.submitter_email }} has been merged to ticket {{ ticket.merged_to.ticket }}. @@ -21,13 +23,14 @@ From now on, please answer on this ticket, or you can include the tag {{ ticket.

This is a courtesy e-mail to let you know that ticket {{ ticket.ticket }} ({{ ticket.title }}) by {{ ticket.submitter_email }} has been merged to ticket {{ ticket.merged_to.ticket }}.

From now on, please answer on this ticket, or you can include the tag {{ ticket.merged_to.ticket }} in your e-mail subject.

""", - locale='en' + locale="en", ) EmailTemplate.objects.using(db_alias).create( - id=EmailTemplate.objects.using(db_alias).order_by('-id').first().id + 1, # because PG sequences are not reset - template_name='merged', - subject='(Fusionné)', - heading='Ticket Fusionné', + id=EmailTemplate.objects.using(db_alias).order_by("-id").first().id + + 1, # because PG sequences are not reset + template_name="merged", + subject="(Fusionné)", + heading="Ticket Fusionné", plain_text="""Bonjour, Ce courriel indicatif permet de vous prévenir que le ticket {{ ticket.ticket }} ("{{ ticket.title }}") par {{ ticket.submitter_email }} a été fusionné au ticket {{ ticket.merged_to.ticket }}. @@ -38,20 +41,19 @@ Veillez à répondre sur ce ticket dorénavant, ou bien inclure la balise {{ tic

Ce courriel indicatif permet de vous prévenir que le ticket {{ ticket.ticket }} ({{ ticket.title }}) par {{ ticket.submitter_email }} a été fusionné au ticket {{ ticket.merged_to.ticket }}.

Veillez à répondre sur ce ticket dorénavant, ou bien inclure la balise {{ ticket.merged_to.ticket }} dans le sujet de votre réponse par mail.

""", - locale='fr' + locale="fr", ) def reverse_func(apps, schema_editor): EmailTemplate = apps.get_model("helpdesk", "EmailTemplate") db_alias = schema_editor.connection.alias - EmailTemplate.objects.using(db_alias).filter(template_name='merged').delete() + EmailTemplate.objects.using(db_alias).filter(template_name="merged").delete() class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0033_ticket_merged_to'), + ("helpdesk", "0033_ticket_merged_to"), ] operations = [ diff --git a/helpdesk/migrations/0035_alter_email_on_ticket_change.py b/helpdesk/migrations/0035_alter_email_on_ticket_change.py index 7cf31029..238aa6bd 100644 --- a/helpdesk/migrations/0035_alter_email_on_ticket_change.py +++ b/helpdesk/migrations/0035_alter_email_on_ticket_change.py @@ -5,15 +5,18 @@ import helpdesk.models class Migration(migrations.Migration): + dependencies = [ + ("helpdesk", "0034_create_email_template_for_merged"), + ] - dependencies = [ - ('helpdesk', '0034_create_email_template_for_merged'), - ] - - operations = [ + operations = [ migrations.AlterField( - model_name='usersettings', - name='email_on_ticket_change', - field=models.BooleanField(default=helpdesk.models.email_on_ticket_change_default, help_text="If you're the ticket owner and the ticket is changed via the web by somebody else,do you want to receive an e-mail?", verbose_name='E-mail me on ticket change?'), + model_name="usersettings", + name="email_on_ticket_change", + field=models.BooleanField( + default=helpdesk.models.email_on_ticket_change_default, + help_text="If you're the ticket owner and the ticket is changed via the web by somebody else,do you want to receive an e-mail?", + verbose_name="E-mail me on ticket change?", + ), ), - ] + ] diff --git a/helpdesk/migrations/0036_add_attachment_validator.py b/helpdesk/migrations/0036_add_attachment_validator.py index aaceb565..1323802f 100644 --- a/helpdesk/migrations/0036_add_attachment_validator.py +++ b/helpdesk/migrations/0036_add_attachment_validator.py @@ -6,20 +6,29 @@ import helpdesk.validators class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0035_alter_email_on_ticket_change'), + ("helpdesk", "0035_alter_email_on_ticket_change"), ] operations = [ migrations.AlterField( - model_name='followupattachment', - name='file', - field=models.FileField(max_length=1000, upload_to=helpdesk.models.attachment_path, validators=[helpdesk.validators.validate_file_extension], verbose_name='File'), + model_name="followupattachment", + name="file", + field=models.FileField( + max_length=1000, + upload_to=helpdesk.models.attachment_path, + validators=[helpdesk.validators.validate_file_extension], + verbose_name="File", + ), ), migrations.AlterField( - model_name='kbiattachment', - name='file', - field=models.FileField(max_length=1000, upload_to=helpdesk.models.attachment_path, validators=[helpdesk.validators.validate_file_extension], verbose_name='File'), + model_name="kbiattachment", + name="file", + field=models.FileField( + max_length=1000, + upload_to=helpdesk.models.attachment_path, + validators=[helpdesk.validators.validate_file_extension], + verbose_name="File", + ), ), ] diff --git a/helpdesk/migrations/0037_alter_queue_email_box_type.py b/helpdesk/migrations/0037_alter_queue_email_box_type.py index cb46d053..78322d47 100644 --- a/helpdesk/migrations/0037_alter_queue_email_box_type.py +++ b/helpdesk/migrations/0037_alter_queue_email_box_type.py @@ -4,15 +4,26 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0036_add_attachment_validator'), + ("helpdesk", "0036_add_attachment_validator"), ] operations = [ migrations.AlterField( - model_name='queue', - name='email_box_type', - field=models.CharField(blank=True, choices=[('pop3', 'POP 3'), ('imap', 'IMAP'), ('oauth', 'IMAP OAUTH'), ('local', 'Local Directory')], help_text='E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.', max_length=5, null=True, verbose_name='E-Mail Box Type'), + model_name="queue", + name="email_box_type", + field=models.CharField( + blank=True, + choices=[ + ("pop3", "POP 3"), + ("imap", "IMAP"), + ("oauth", "IMAP OAUTH"), + ("local", "Local Directory"), + ], + help_text="E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.", + max_length=5, + null=True, + verbose_name="E-Mail Box Type", + ), ), ] diff --git a/helpdesk/migrations/0038_checklist_checklisttemplate_checklisttask.py b/helpdesk/migrations/0038_checklist_checklisttemplate_checklisttask.py index a3c80616..dcf4d9f2 100644 --- a/helpdesk/migrations/0038_checklist_checklisttemplate_checklisttask.py +++ b/helpdesk/migrations/0038_checklist_checklisttemplate_checklisttask.py @@ -6,49 +6,107 @@ import helpdesk.models class Migration(migrations.Migration): - dependencies = [ - ('helpdesk', '0037_alter_queue_email_box_type'), + ("helpdesk", "0037_alter_queue_email_box_type"), ] operations = [ migrations.CreateModel( - name='Checklist', + name="Checklist", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, verbose_name='Name')), - ('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to='helpdesk.ticket', verbose_name='Ticket')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, verbose_name="Name")), + ( + "ticket", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="checklists", + to="helpdesk.ticket", + verbose_name="Ticket", + ), + ), ], options={ - 'verbose_name': 'Checklist', - 'verbose_name_plural': 'Checklists', + "verbose_name": "Checklist", + "verbose_name_plural": "Checklists", }, ), migrations.CreateModel( - name='ChecklistTemplate', + name="ChecklistTemplate", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, verbose_name='Name')), - ('task_list', models.JSONField(validators=[helpdesk.models.is_a_list_without_empty_element], verbose_name='Task List')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, verbose_name="Name")), + ( + "task_list", + models.JSONField( + validators=[helpdesk.models.is_a_list_without_empty_element], + verbose_name="Task List", + ), + ), ], options={ - 'verbose_name': 'Checklist Template', - 'verbose_name_plural': 'Checklist Templates', + "verbose_name": "Checklist Template", + "verbose_name_plural": "Checklist Templates", }, ), migrations.CreateModel( - name='ChecklistTask', + name="ChecklistTask", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description', models.CharField(max_length=250, verbose_name='Description')), - ('completion_date', models.DateTimeField(blank=True, null=True, verbose_name='Completion Date')), - ('position', models.PositiveSmallIntegerField(db_index=True, verbose_name='Position')), - ('checklist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='helpdesk.checklist', verbose_name='Checklist')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "description", + models.CharField(max_length=250, verbose_name="Description"), + ), + ( + "completion_date", + models.DateTimeField( + blank=True, null=True, verbose_name="Completion Date" + ), + ), + ( + "position", + models.PositiveSmallIntegerField( + db_index=True, verbose_name="Position" + ), + ), + ( + "checklist", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tasks", + to="helpdesk.checklist", + verbose_name="Checklist", + ), + ), ], options={ - 'verbose_name': 'Checklist Task', - 'verbose_name_plural': 'Checklist Tasks', - 'ordering': ('position',), + "verbose_name": "Checklist Task", + "verbose_name_plural": "Checklist Tasks", + "ordering": ("position",), }, ), ] diff --git a/helpdesk/models.py b/helpdesk/models.py index a9fca8ad..51a028ac 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -7,7 +7,6 @@ models.py - Model (and hence database) definitions. This is the core of the helpdesk structure. """ - from .lib import format_time_spent, convert_value, daily_time_spent_calculation from .templated_email import send_templated_mail from .validators import validate_file_extension @@ -34,24 +33,24 @@ import uuid class EscapeHtml(Extension): def extendMarkdown(self, md): - md.preprocessors.deregister('html_block') - md.inlinePatterns.deregister('html') + md.preprocessors.deregister("html_block") + md.inlinePatterns.deregister("html") def get_markdown(text): if not text: return "" - pattern = r'([\[\s\S\]]*?)\(([\s\S]*?):([\s\S]*?)\)' + pattern = r"([\[\s\S\]]*?)\(([\s\S]*?):([\s\S]*?)\)" # Regex check if re.match(pattern, text): # get get value of group regex scheme = re.search(pattern, text, re.IGNORECASE).group(2) # scheme check if scheme in helpdesk_settings.ALLOWED_URL_SCHEMES: - replacement = '\\1(\\2:\\3)' + replacement = "\\1(\\2:\\3)" else: - replacement = '\\1(\\3)' + replacement = "\\1(\\3)" text = re.sub(pattern, replacement, text, flags=re.IGNORECASE) @@ -59,9 +58,10 @@ def get_markdown(text): markdown( text, extensions=[ - EscapeHtml(), 'markdown.extensions.nl2br', - 'markdown.extensions.fenced_code' - ] + EscapeHtml(), + "markdown.extensions.nl2br", + "markdown.extensions.fenced_code", + ], ) ) @@ -77,183 +77,208 @@ class Queue(models.Model): """ title = models.CharField( - _('Title'), + _("Title"), max_length=100, ) slug = models.SlugField( - _('Slug'), + _("Slug"), max_length=50, unique=True, - help_text=_('This slug is used when building ticket ID\'s. Once set, ' - 'try not to change it or e-mailing may get messy.'), + help_text=_( + "This slug is used when building ticket ID's. Once set, " + "try not to change it or e-mailing may get messy." + ), ) email_address = models.EmailField( - _('E-Mail Address'), + _("E-Mail Address"), blank=True, null=True, - help_text=_('All outgoing e-mails for this queue will use this e-mail ' - 'address. If you use IMAP or POP3, this should be the e-mail ' - 'address for that mailbox.'), + help_text=_( + "All outgoing e-mails for this queue will use this e-mail " + "address. If you use IMAP or POP3, this should be the e-mail " + "address for that mailbox." + ), ) locale = models.CharField( - _('Locale'), + _("Locale"), max_length=10, blank=True, null=True, - help_text=_('Locale of this queue. All correspondence in this ' - 'queue will be in this language.'), + help_text=_( + "Locale of this queue. All correspondence in this " + "queue will be in this language." + ), ) allow_public_submission = models.BooleanField( - _('Allow Public Submission?'), + _("Allow Public Submission?"), blank=True, default=False, - help_text=_( - 'Should this queue be listed on the public submission form?'), + help_text=_("Should this queue be listed on the public submission form?"), ) allow_email_submission = models.BooleanField( - _('Allow E-Mail Submission?'), + _("Allow E-Mail Submission?"), blank=True, default=False, - help_text=_('Do you want to poll the e-mail box below for new ' - 'tickets?'), + help_text=_("Do you want to poll the e-mail box below for new tickets?"), ) escalate_days = models.IntegerField( - _('Escalation Days'), + _("Escalation Days"), blank=True, null=True, - help_text=_('For tickets which are not held, how often do you wish to ' - 'increase their priority? Set to 0 for no escalation.'), + help_text=_( + "For tickets which are not held, how often do you wish to " + "increase their priority? Set to 0 for no escalation." + ), ) new_ticket_cc = models.CharField( - _('New Ticket CC Address'), + _("New Ticket CC Address"), blank=True, null=True, max_length=200, - help_text=_('If an e-mail address is entered here, then it will ' - 'receive notification of all new tickets created for this queue. ' - 'Enter a comma between multiple e-mail addresses.'), + help_text=_( + "If an e-mail address is entered here, then it will " + "receive notification of all new tickets created for this queue. " + "Enter a comma between multiple e-mail addresses." + ), ) updated_ticket_cc = models.CharField( - _('Updated Ticket CC Address'), + _("Updated Ticket CC Address"), blank=True, null=True, max_length=200, - help_text=_('If an e-mail address is entered here, then it will ' - 'receive notification of all activity (new tickets, closed ' - 'tickets, updates, reassignments, etc) for this queue. Separate ' - 'multiple addresses with a comma.'), + help_text=_( + "If an e-mail address is entered here, then it will " + "receive notification of all activity (new tickets, closed " + "tickets, updates, reassignments, etc) for this queue. Separate " + "multiple addresses with a comma." + ), ) enable_notifications_on_email_events = models.BooleanField( - _('Notify contacts when email updates arrive'), + _("Notify contacts when email updates arrive"), blank=True, default=False, - help_text=_('When an email arrives to either create a ticket or to ' - 'interact with an existing discussion. Should email notifications be sent ? ' - 'Note: the new_ticket_cc and updated_ticket_cc work independently of this feature'), + help_text=_( + "When an email arrives to either create a ticket or to " + "interact with an existing discussion. Should email notifications be sent ? " + "Note: the new_ticket_cc and updated_ticket_cc work independently of this feature" + ), ) email_box_type = models.CharField( - _('E-Mail Box Type'), + _("E-Mail Box Type"), max_length=5, - choices=(('pop3', _('POP 3')), - ('imap', _('IMAP')), - ('oauth', _('IMAP OAUTH')), - ('local', _('Local Directory'))), + choices=( + ("pop3", _("POP 3")), + ("imap", _("IMAP")), + ("oauth", _("IMAP OAUTH")), + ("local", _("Local Directory")), + ), blank=True, null=True, - help_text=_('E-Mail server type for creating tickets automatically ' - 'from a mailbox - both POP3 and IMAP are supported, as well as ' - 'reading from a local directory.'), + help_text=_( + "E-Mail server type for creating tickets automatically " + "from a mailbox - both POP3 and IMAP are supported, as well as " + "reading from a local directory." + ), ) email_box_host = models.CharField( - _('E-Mail Hostname'), + _("E-Mail Hostname"), max_length=200, blank=True, null=True, - help_text=_('Your e-mail server address - either the domain name or ' - 'IP address. May be "localhost".'), + help_text=_( + "Your e-mail server address - either the domain name or " + 'IP address. May be "localhost".' + ), ) email_box_port = models.IntegerField( - _('E-Mail Port'), + _("E-Mail Port"), blank=True, null=True, - help_text=_('Port number to use for accessing e-mail. Default for ' - 'POP3 is "110", and for IMAP is "143". This may differ on some ' - 'servers. Leave it blank to use the defaults.'), + help_text=_( + "Port number to use for accessing e-mail. Default for " + 'POP3 is "110", and for IMAP is "143". This may differ on some ' + "servers. Leave it blank to use the defaults." + ), ) email_box_ssl = models.BooleanField( - _('Use SSL for E-Mail?'), + _("Use SSL for E-Mail?"), blank=True, default=False, - help_text=_('Whether to use SSL for IMAP or POP3 - the default ports ' - 'when using SSL are 993 for IMAP and 995 for POP3.'), + help_text=_( + "Whether to use SSL for IMAP or POP3 - the default ports " + "when using SSL are 993 for IMAP and 995 for POP3." + ), ) email_box_user = models.CharField( - _('E-Mail Username'), + _("E-Mail Username"), max_length=200, blank=True, null=True, - help_text=_('Username for accessing this mailbox.'), + help_text=_("Username for accessing this mailbox."), ) email_box_pass = models.CharField( - _('E-Mail Password'), + _("E-Mail Password"), max_length=200, blank=True, null=True, - help_text=_('Password for the above username'), + help_text=_("Password for the above username"), ) email_box_imap_folder = models.CharField( - _('IMAP Folder'), + _("IMAP Folder"), max_length=100, blank=True, null=True, - help_text=_('If using IMAP, what folder do you wish to fetch messages ' - 'from? This allows you to use one IMAP account for multiple ' - 'queues, by filtering messages on your IMAP server into separate ' - 'folders. Default: INBOX.'), + help_text=_( + "If using IMAP, what folder do you wish to fetch messages " + "from? This allows you to use one IMAP account for multiple " + "queues, by filtering messages on your IMAP server into separate " + "folders. Default: INBOX." + ), ) email_box_local_dir = models.CharField( - _('E-Mail Local Directory'), + _("E-Mail Local Directory"), max_length=200, blank=True, null=True, - help_text=_('If using a local directory, what directory path do you ' - 'wish to poll for new email? ' - 'Example: /var/lib/mail/helpdesk/'), + help_text=_( + "If using a local directory, what directory path do you " + "wish to poll for new email? " + "Example: /var/lib/mail/helpdesk/" + ), ) permission_name = models.CharField( - _('Django auth permission name'), + _("Django auth permission name"), max_length=72, # based on prepare_permission_name() pre-pending chars to slug blank=True, null=True, editable=False, - help_text=_('Name used in the django.contrib.auth permission system'), + help_text=_("Name used in the django.contrib.auth permission system"), ) email_box_interval = models.IntegerField( - _('E-Mail Check Interval'), - help_text=_( - 'How often do you wish to check this mailbox? (in Minutes)'), + _("E-Mail Check Interval"), + help_text=_("How often do you wish to check this mailbox? (in Minutes)"), blank=True, null=True, - default='5', + default="5", ) email_box_last_check = models.DateTimeField( @@ -264,79 +289,82 @@ class Queue(models.Model): ) socks_proxy_type = models.CharField( - _('Socks Proxy Type'), + _("Socks Proxy Type"), max_length=8, - choices=(('socks4', _('SOCKS4')), ('socks5', _('SOCKS5'))), + choices=(("socks4", _("SOCKS4")), ("socks5", _("SOCKS5"))), blank=True, null=True, help_text=_( - 'SOCKS4 or SOCKS5 allows you to proxy your connections through a SOCKS server.'), + "SOCKS4 or SOCKS5 allows you to proxy your connections through a SOCKS server." + ), ) socks_proxy_host = models.GenericIPAddressField( - _('Socks Proxy Host'), + _("Socks Proxy Host"), blank=True, null=True, - help_text=_('Socks proxy IP address. Default: 127.0.0.1'), + help_text=_("Socks proxy IP address. Default: 127.0.0.1"), ) socks_proxy_port = models.IntegerField( - _('Socks Proxy Port'), + _("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( - _('Logging Type'), + _("Logging Type"), max_length=5, choices=( - ('none', _('None')), - ('debug', _('Debug')), - ('info', _('Information')), - ('warn', _('Warning')), - ('error', _('Error')), - ('crit', _('Critical')) + ("none", _("None")), + ("debug", _("Debug")), + ("info", _("Information")), + ("warn", _("Warning")), + ("error", _("Error")), + ("crit", _("Critical")), ), blank=True, null=True, - help_text=_('Set the default logging level. All messages at that ' - 'level or above will be logged to the directory set ' - 'below. If no level is set, logging will be disabled.'), + help_text=_( + "Set the default logging level. All messages at that " + "level or above will be logged to the directory set " + "below. If no level is set, logging will be disabled." + ), ) logging_dir = models.CharField( - _('Logging Directory'), + _("Logging Directory"), max_length=200, blank=True, null=True, - help_text=_('If logging is enabled, what directory should we use to ' - 'store log files for this queue? ' - 'The standard logging mechanims are used if no directory is set'), + help_text=_( + "If logging is enabled, what directory should we use to " + "store log files for this queue? " + "The standard logging mechanims are used if no directory is set" + ), ) default_owner = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, - related_name='default_owner', + related_name="default_owner", blank=True, null=True, - verbose_name=_('Default owner'), + verbose_name=_("Default owner"), ) dedicated_time = models.DurationField( - help_text=_("Time to be spent on this Queue in total"), - blank=True, null=True + help_text=_("Time to be spent on this Queue in total"), blank=True, null=True ) def __str__(self): return "%s" % self.title class Meta: - ordering = ('title',) - verbose_name = _('Queue') - verbose_name_plural = _('Queues') + ordering = ("title",) + verbose_name = _("Queue") + verbose_name_plural = _("Queues") def _from_address(self): """ @@ -347,14 +375,18 @@ 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) + ".*<(?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 + return "NO QUEUE EMAIL ADDRESS DEFINED %s" % settings.DEFAULT_FROM_EMAIL else: - return u'NO QUEUE EMAIL ADDRESS DEFINED <%s>' % settings.DEFAULT_FROM_EMAIL + return ( + "NO QUEUE EMAIL ADDRESS DEFINED <%s>" % settings.DEFAULT_FROM_EMAIL + ) else: - return u'%s <%s>' % (self.title, self.email_address) + return "%s <%s>" % (self.title, self.email_address) + from_address = property(_from_address) @property @@ -362,8 +394,10 @@ class Queue(models.Model): """Return back total time spent on the ticket. This is calculated value based on total sum from all FollowUps """ - res = FollowUp.objects.filter(ticket__queue=self).aggregate(models.Sum('time_spent')) - return res.get('time_spent__sum', datetime.timedelta(0)) + res = FollowUp.objects.filter(ticket__queue=self).aggregate( + models.Sum("time_spent") + ) + return res.get("time_spent__sum", datetime.timedelta(0)) @property def time_spent_formated(self): @@ -379,12 +413,12 @@ class Queue(models.Model): return basename def save(self, *args, **kwargs): - if self.email_box_type == 'imap' and not self.email_box_imap_folder: - self.email_box_imap_folder = 'INBOX' + if self.email_box_type == "imap" and not self.email_box_imap_folder: + self.email_box_imap_folder = "INBOX" if self.socks_proxy_type: if not self.socks_proxy_host: - self.socks_proxy_host = '127.0.0.1' + self.socks_proxy_host = "127.0.0.1" if not self.socks_proxy_port: self.socks_proxy_port = 9150 else: @@ -392,13 +426,13 @@ class Queue(models.Model): self.socks_proxy_port = None if not self.email_box_port: - if self.email_box_type == 'imap' and self.email_box_ssl: + if self.email_box_type == "imap" and self.email_box_ssl: self.email_box_port = 993 - elif self.email_box_type == 'imap' and not self.email_box_ssl: + elif self.email_box_type == "imap" and not self.email_box_ssl: self.email_box_port = 143 - elif self.email_box_type == 'pop3' and self.email_box_ssl: + elif self.email_box_type == "pop3" and self.email_box_ssl: self.email_box_port = 995 - elif self.email_box_type == 'pop3' and not self.email_box_ssl: + elif self.email_box_type == "pop3" and not self.email_box_ssl: self.email_box_port = 110 if not self.id: @@ -461,83 +495,84 @@ class Ticket(models.Model): PRIORITY_CHOICES = helpdesk_settings.TICKET_PRIORITY_CHOICES title = models.CharField( - _('Title'), + _("Title"), max_length=200, ) queue = models.ForeignKey( Queue, on_delete=models.CASCADE, - verbose_name=_('Queue'), + verbose_name=_("Queue"), ) created = models.DateTimeField( - _('Created'), + _("Created"), blank=True, - help_text=_('Date this ticket was first created'), + help_text=_("Date this ticket was first created"), ) modified = models.DateTimeField( - _('Modified'), + _("Modified"), blank=True, - help_text=_('Date this ticket was most recently changed.'), + help_text=_("Date this ticket was most recently changed."), ) submitter_email = models.EmailField( - _('Submitter E-Mail'), + _("Submitter E-Mail"), blank=True, null=True, - help_text=_('The submitter will receive an email for all public ' - 'follow-ups left for this task.'), + help_text=_( + "The submitter will receive an email for all public " + "follow-ups left for this task." + ), ) assigned_to = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, - related_name='assigned_to', + related_name="assigned_to", blank=True, null=True, - verbose_name=_('Assigned to'), + verbose_name=_("Assigned to"), ) status = models.IntegerField( - _('Status'), + _("Status"), choices=STATUS_CHOICES, default=OPEN_STATUS, ) on_hold = models.BooleanField( - _('On Hold'), + _("On Hold"), blank=True, default=False, - help_text=_( - 'If a ticket is on hold, it will not automatically be escalated.'), + help_text=_("If a ticket is on hold, it will not automatically be escalated."), ) description = models.TextField( - _('Description'), + _("Description"), blank=True, null=True, - help_text=_('The content of the customers query.'), + help_text=_("The content of the customers query."), ) resolution = models.TextField( - _('Resolution'), + _("Resolution"), blank=True, null=True, - help_text=_('The resolution provided to the customer by our staff.'), + help_text=_("The resolution provided to the customer by our staff."), ) priority = models.IntegerField( - _('Priority'), + _("Priority"), choices=PRIORITY_CHOICES, default=3, blank=3, - help_text=_('1 = Highest Priority, 5 = Low Priority'), + help_text=_("1 = Highest Priority, 5 = Low Priority"), ) due_date = models.DateTimeField( - _('Due on'), + _("Due on"), blank=True, null=True, ) @@ -546,8 +581,10 @@ class Ticket(models.Model): blank=True, null=True, editable=False, - help_text=_('The date this ticket was last escalated - updated ' - 'automatically by management/commands/escalate_tickets.py.'), + help_text=_( + "The date this ticket was last escalated - updated " + "automatically by management/commands/escalate_tickets.py." + ), ) secret_key = models.CharField( @@ -562,16 +599,17 @@ class Ticket(models.Model): null=True, on_delete=models.CASCADE, verbose_name=_( - 'Knowledge base item the user was viewing when they created this ticket.'), + "Knowledge base item the user was viewing when they created this ticket." + ), ) merged_to = models.ForeignKey( - 'self', - verbose_name=_('merged to'), - related_name='merged_tickets', + "self", + verbose_name=_("merged to"), + related_name="merged_tickets", on_delete=models.CASCADE, null=True, - blank=True + blank=True, ) @property @@ -579,8 +617,8 @@ class Ticket(models.Model): """Return back total time spent on the ticket. This is calculated value based on total sum from all FollowUps """ - res = FollowUp.objects.filter(ticket=self).aggregate(models.Sum('time_spent')) - return res.get('time_spent__sum', datetime.timedelta(0)) + res = FollowUp.objects.filter(ticket=self).aggregate(models.Sum("time_spent")) + return res.get("time_spent__sum", datetime.timedelta(0)) @property def time_spent_formated(self): @@ -626,42 +664,50 @@ class Ticket(models.Model): 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) + template, + context, + recipient, + sender=self.queue.from_address, + **kwargs, + ) recipients.add(recipient) - send('submitter', self.submitter_email) - send('ticket_cc', self.queue.updated_ticket_cc) - send('new_ticket_cc', self.queue.new_ticket_cc) + send("submitter", self.submitter_email) + send("ticket_cc", self.queue.updated_ticket_cc) + send("new_ticket_cc", self.queue.new_ticket_cc) if self.assigned_to: - send('assigned_to', self.assigned_to.email) + send("assigned_to", self.assigned_to.email) if self.queue.enable_notifications_on_email_events: for cc in self.ticketcc_set.all(): - send('ticket_cc', cc.email_address) + send("ticket_cc", cc.email_address) return recipients def _get_assigned_to(self): - """ Custom property to allow us to easily print 'Unassigned' if a + """Custom property to allow us to easily print 'Unassigned' if a ticket has no owner, or the users name if it's assigned. If the user - has a full name configured, we use that, otherwise their username. """ + has a full name configured, we use that, otherwise their username.""" if not self.assigned_to: - return _('Unassigned') + return _("Unassigned") else: if self.assigned_to.get_full_name(): return self.assigned_to.get_full_name() else: return self.assigned_to.get_username() + get_assigned_to = property(_get_assigned_to) def _get_ticket(self): - """ A user-friendly ticket ID, which is a combination of ticket ID - and queue slug. This is generally used in e-mail subjects. """ + """A user-friendly ticket ID, which is a combination of ticket ID + and queue slug. This is generally used in e-mail subjects.""" + + return "[%s]" % self.ticket_for_url - return u"[%s]" % self.ticket_for_url ticket = property(_get_ticket) def _get_ticket_for_url(self): - """ A URL-friendly ticket ID, used in links. """ - return u"%s-%s" % (self.queue.slug, self.id) + """A URL-friendly ticket ID, used in links.""" + return "%s-%s" % (self.queue.slug, self.id) + ticket_for_url = property(_get_ticket_for_url) def _get_priority_css_class(self): @@ -676,19 +722,21 @@ class Ticket(models.Model): return "success" else: return "" + get_priority_css_class = property(_get_priority_css_class) def _get_status(self): """ Displays the ticket status, with an "On Hold" message if needed. """ - held_msg = '' + held_msg = "" if self.on_hold: - held_msg = _(' - On Hold') - dep_msg = '' + held_msg = _(" - On Hold") + dep_msg = "" if not self.can_be_resolved: - dep_msg = _(' - Open dependencies') - return u'%s%s%s' % (self.get_status_display(), held_msg, dep_msg) + dep_msg = _(" - Open dependencies") + return "%s%s%s" % (self.get_status_display(), held_msg, dep_msg) + get_status = property(_get_status) def _get_allowed_status_flow(self): @@ -699,12 +747,15 @@ class Ticket(models.Model): if status_id_list: # keep defined statuses in order and add labels for display status_dict = dict(helpdesk_settings.TICKET_STATUS_CHOICES) - new_statuses = [(status_id, status_dict.get(status_id, _('No label'))) - for status_id in status_id_list] + new_statuses = [ + (status_id, status_dict.get(status_id, _("No label"))) + for status_id in status_id_list + ] else: # defaults to all choices if status was not mapped new_statuses = helpdesk_settings.TICKET_STATUS_CHOICES return new_statuses + get_allowed_status_flow = property(_get_allowed_status_flow) def _get_ticket_url(self): @@ -715,22 +766,24 @@ class Ticket(models.Model): from django.contrib.sites.models import Site from django.core.exceptions import ImproperlyConfigured from django.urls import reverse + try: site = Site.objects.get_current() except ImproperlyConfigured: - site = Site(domain='configure-django-sites.com') + site = Site(domain="configure-django-sites.com") if helpdesk_settings.HELPDESK_USE_HTTPS_IN_EMAIL_LINK: - protocol = 'https' + protocol = "https" else: - protocol = 'http' - return u"%s://%s%s?ticket=%s&email=%s&key=%s" % ( + protocol = "http" + return "%s://%s%s?ticket=%s&email=%s&key=%s" % ( protocol, site.domain, - reverse('helpdesk:public_view'), + reverse("helpdesk:public_view"), self.ticket_for_url, self.submitter_email, - self.secret_key + self.secret_key, ) + ticket_url = property(_get_ticket_url) def _get_staff_url(self): @@ -741,20 +794,21 @@ class Ticket(models.Model): from django.contrib.sites.models import Site from django.core.exceptions import ImproperlyConfigured from django.urls import reverse + try: site = Site.objects.get_current() except ImproperlyConfigured: - site = Site(domain='configure-django-sites.com') + site = Site(domain="configure-django-sites.com") if helpdesk_settings.HELPDESK_USE_HTTPS_IN_EMAIL_LINK: - protocol = 'https' + protocol = "https" else: - protocol = 'http' - return u"%s://%s%s" % ( + protocol = "http" + return "%s://%s%s" % ( protocol, site.domain, - reverse('helpdesk:view', - args=[self.id]) + reverse("helpdesk:view", args=[self.id]), ) + staff_url = property(_get_staff_url) def _can_be_resolved(self): @@ -763,8 +817,13 @@ class Ticket(models.Model): True = any dependencies are resolved False = There are non-resolved dependencies """ - return TicketDependency.objects.filter(ticket=self).filter( - depends_on__status__in=Ticket.OPEN_STATUSES).count() == 0 + return ( + TicketDependency.objects.filter(ticket=self) + .filter(depends_on__status__in=Ticket.OPEN_STATUSES) + .count() + == 0 + ) + can_be_resolved = property(_can_be_resolved) def get_submitter_userprofile(self): @@ -776,16 +835,17 @@ class Ticket(models.Model): class Meta: get_latest_by = "created" - ordering = ('id',) - verbose_name = _('Ticket') - verbose_name_plural = _('Tickets') + ordering = ("id",) + verbose_name = _("Ticket") + verbose_name_plural = _("Tickets") def __str__(self): - return '%s %s' % (self.id, self.title) + return "%s %s" % (self.id, self.title) def get_absolute_url(self): from django.urls import reverse - return reverse('helpdesk:view', args=(self.id,)) + + return reverse("helpdesk:view", args=(self.id,)) def save(self, *args, **kwargs): if not self.id: @@ -806,8 +866,8 @@ class Ticket(models.Model): def queue_and_id_from_query(query): # Apply the opposite logic here compared to self._get_ticket_for_url # Ensure that queues with '-' in them will work - parts = query.split('-') - queue = '-'.join(parts[0:-1]) + parts = query.split("-") + queue = "-".join(parts[0:-1]) return queue, parts[-1] def get_markdown(self): @@ -838,7 +898,8 @@ class Ticket(models.Model): return elif not email: raise ValueError( - 'You must provide at least one parameter to get the email from') + "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()] @@ -851,7 +912,7 @@ class Ticket(models.Model): if email not in ticket_emails: if ticketcc: ticketcc.ticket = self - ticketcc.save(update_fields=['ticket']) + ticketcc.save(update_fields=["ticket"]) elif user: ticketcc = self.ticketcc_set.create(user=user) else: @@ -864,16 +925,15 @@ class Ticket(models.Model): value = self.ticketcustomfieldvalue_set.get(field=field).value except TicketCustomFieldValue.DoesNotExist: value = None - setattr(self, 'custom_%s' % field.name, value) + setattr(self, "custom_%s" % field.name, value) def save_custom_field_values(self, data): for field, value in data.items(): - if field.startswith('custom_'): - field_name = field.replace('custom_', '', 1) + if field.startswith("custom_"): + field_name = field.replace("custom_", "", 1) customfield = CustomField.objects.get(name=field_name) cfv, created = self.ticketcustomfieldvalue_set.get_or_create( - field=customfield, - defaults={'value': convert_value(value)} + field=customfield, defaults={"value": convert_value(value)} ) if not created: cfv.value = convert_value(value) @@ -881,7 +941,6 @@ class Ticket(models.Model): class FollowUpManager(models.Manager): - def private_followups(self): return self.filter(public=False) @@ -905,34 +964,31 @@ class FollowUp(models.Model): ticket = models.ForeignKey( Ticket, on_delete=models.CASCADE, - verbose_name=_('Ticket'), + verbose_name=_("Ticket"), ) - date = models.DateTimeField( - _('Date'), - default=timezone.now - ) + date = models.DateTimeField(_("Date"), default=timezone.now) title = models.CharField( - _('Title'), + _("Title"), max_length=200, blank=True, null=True, ) comment = models.TextField( - _('Comment'), + _("Comment"), blank=True, null=True, ) public = models.BooleanField( - _('Public'), + _("Public"), blank=True, default=False, help_text=_( - 'Public tickets are viewable by the submitter and all ' - 'staff, but non-public tickets can only be seen by staff.' + "Public tickets are viewable by the submitter and all " + "staff, but non-public tickets can only be seen by staff." ), ) @@ -941,19 +997,19 @@ class FollowUp(models.Model): on_delete=models.CASCADE, blank=True, null=True, - verbose_name=_('User'), + verbose_name=_("User"), ) new_status = models.IntegerField( - _('New Status'), + _("New Status"), choices=Ticket.STATUS_CHOICES, blank=True, null=True, - help_text=_('If the status was changed, what was it changed to?'), + help_text=_("If the status was changed, what was it changed to?"), ) message_id = models.CharField( - _('E-Mail ID'), + _("E-Mail ID"), max_length=256, blank=True, null=True, @@ -964,20 +1020,19 @@ class FollowUp(models.Model): objects = FollowUpManager() time_spent = models.DurationField( - help_text=_("Time spent on this follow up"), - blank=True, null=True + help_text=_("Time spent on this follow up"), blank=True, null=True ) class Meta: - ordering = ('date',) - verbose_name = _('Follow-up') - verbose_name_plural = _('Follow-ups') + ordering = ("date",) + verbose_name = _("Follow-up") + verbose_name_plural = _("Follow-ups") def __str__(self): - return '%s' % self.title + return "%s" % self.title def get_absolute_url(self): - return u"%s#followup%s" % (self.ticket.get_absolute_url(), self.id) + return "%s#followup%s" % (self.ticket.get_absolute_url(), self.id) def save(self, *args, **kwargs): self.ticket.modified = timezone.now() @@ -985,7 +1040,7 @@ class FollowUp(models.Model): if helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO and not self.time_spent: self.time_spent = self.time_spent_calculation() - + super(FollowUp, self).save(*args, **kwargs) def get_markdown(self): @@ -997,12 +1052,12 @@ class FollowUp(models.Model): def time_spent_calculation(self): "Returns timedelta according to rules settings." - + open_hours = helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS holidays = helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS exclude_statuses = helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES exclude_queues = helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES - + # queryset for this ticket previous follow-ups prev_fup_qs = self.ticket.followup_set.all() if self.id: @@ -1018,22 +1073,23 @@ class FollowUp(models.Model): prev_status = prev_fup.new_status except ObjectDoesNotExist: prev_status = self.ticket.status - + # don't calculate status exclusions if prev_status in exclude_statuses: return datetime.timedelta(seconds=0) - + # find the previous queue for exclusion check if exclude_queues: try: - prev_fup_ids = prev_fup_qs.values_list('id', flat=True) - prev_queue_change = TicketChange.objects.filter(followup_id__in=prev_fup_ids, - field=_('Queue')).latest('id') + prev_fup_ids = prev_fup_qs.values_list("id", flat=True) + prev_queue_change = TicketChange.objects.filter( + followup_id__in=prev_fup_ids, field=_("Queue") + ).latest("id") prev_queue = Queue.objects.get(pk=prev_queue_change.new_value) prev_queue_slug = prev_queue.slug except ObjectDoesNotExist: prev_queue_slug = self.ticket.queue.slug - + # don't calculate queue exclusions if prev_queue_slug in exclude_queues: return datetime.timedelta(seconds=0) @@ -1051,7 +1107,7 @@ class FollowUp(models.Model): # latest time is current follow-up date latest = self.date - + # split time interval by days days = latest.toordinal() - earliest.toordinal() for day in range(days + 1): @@ -1061,20 +1117,27 @@ class FollowUp(models.Model): # close single day case end_day_time = latest else: - end_day_time = earliest.replace(hour=23, minute=59, second=59, microsecond=999999) + end_day_time = earliest.replace( + hour=23, minute=59, second=59, microsecond=999999 + ) elif day == days: start_day_time = latest.replace(hour=0, minute=0, second=0) end_day_time = latest else: middle_day_time = earliest + datetime.timedelta(days=day) start_day_time = middle_day_time.replace(hour=0, minute=0, second=0) - end_day_time = middle_day_time.replace(hour=23, minute=59, second=59, microsecond=999999) - + end_day_time = middle_day_time.replace( + hour=23, minute=59, second=59, microsecond=999999 + ) + if start_day_time.strftime("%Y-%m-%d") not in holidays: - time_spent_seconds += daily_time_spent_calculation(start_day_time, end_day_time, open_hours) + time_spent_seconds += daily_time_spent_calculation( + start_day_time, end_day_time, open_hours + ) return datetime.timedelta(seconds=time_spent_seconds) + class TicketChange(models.Model): """ For each FollowUp, any changes to the parent ticket (eg Title, Priority, @@ -1084,42 +1147,42 @@ class TicketChange(models.Model): followup = models.ForeignKey( FollowUp, on_delete=models.CASCADE, - verbose_name=_('Follow-up'), + verbose_name=_("Follow-up"), ) field = models.CharField( - _('Field'), + _("Field"), max_length=100, ) old_value = models.TextField( - _('Old Value'), + _("Old Value"), blank=True, null=True, ) new_value = models.TextField( - _('New Value'), + _("New Value"), blank=True, null=True, ) def __str__(self): - out = '%s ' % self.field + out = "%s " % self.field if not self.new_value: - out += gettext('removed') + out += gettext("removed") elif not self.old_value: - out += gettext('set to %s') % self.new_value + out += gettext("set to %s") % self.new_value else: out += gettext('changed from "%(old_value)s" to "%(new_value)s"') % { - 'old_value': self.old_value, - 'new_value': self.new_value + "old_value": self.old_value, + "new_value": self.new_value, } return out class Meta: - verbose_name = _('Ticket change') - verbose_name_plural = _('Ticket changes') + verbose_name = _("Ticket change") + verbose_name_plural = _("Ticket changes") def attachment_path(instance, filename): @@ -1134,35 +1197,34 @@ class Attachment(models.Model): """ file = models.FileField( - _('File'), + _("File"), upload_to=attachment_path, max_length=1000, - validators=[validate_file_extension] + validators=[validate_file_extension], ) filename = models.CharField( - _('Filename'), + _("Filename"), blank=True, max_length=1000, ) mime_type = models.CharField( - _('MIME Type'), + _("MIME Type"), blank=True, max_length=255, ) size = models.IntegerField( - _('Size'), + _("Size"), blank=True, - help_text=_('Size of this file in bytes'), + help_text=_("Size of this file in bytes"), ) def __str__(self): - return '%s' % self.filename + return "%s" % self.filename def save(self, *args, **kwargs): - if not self.size: self.size = self.get_size() @@ -1170,9 +1232,10 @@ class Attachment(models.Model): self.filename = self.get_filename() if not self.mime_type: - self.mime_type = \ - mimetypes.guess_type(self.filename, strict=False)[0] or \ - 'application/octet-stream' + self.mime_type = ( + mimetypes.guess_type(self.filename, strict=False)[0] + or "application/octet-stream" + ) return super(Attachment, self).save(*args, **kwargs) @@ -1191,48 +1254,51 @@ class Attachment(models.Model): ) class Meta: - ordering = ('filename',) - verbose_name = _('Attachment') - verbose_name_plural = _('Attachments') + ordering = ("filename",) + verbose_name = _("Attachment") + verbose_name_plural = _("Attachments") abstract = True class FollowUpAttachment(Attachment): - followup = models.ForeignKey( FollowUp, on_delete=models.CASCADE, - verbose_name=_('Follow-up'), + verbose_name=_("Follow-up"), ) def attachment_path(self, filename): - - path = 'helpdesk/attachments/{ticket_for_url}-{secret_key}/{id_}'.format( + path = "helpdesk/attachments/{ticket_for_url}-{secret_key}/{id_}".format( ticket_for_url=self.followup.ticket.ticket_for_url, secret_key=self.followup.ticket.secret_key, - id_=self.followup.id) + id_=self.followup.id, + ) att_path = os.path.join(settings.MEDIA_ROOT, path) - if settings.DEFAULT_FILE_STORAGE == "django.core.files.storage.FileSystemStorage": + if ( + settings.DEFAULT_FILE_STORAGE + == "django.core.files.storage.FileSystemStorage" + ): if not os.path.exists(att_path): os.makedirs(att_path, helpdesk_settings.HELPDESK_ATTACHMENT_DIR_PERMS) return os.path.join(path, filename) class KBIAttachment(Attachment): - kbitem = models.ForeignKey( "KBItem", on_delete=models.CASCADE, - verbose_name=_('Knowledge base item'), + verbose_name=_("Knowledge base item"), ) def attachment_path(self, filename): - - path = 'helpdesk/attachments/kb/{category}/{kbi}'.format( - category=self.kbitem.category, - kbi=self.kbitem.id) + path = "helpdesk/attachments/kb/{category}/{kbi}".format( + category=self.kbitem.category, kbi=self.kbitem.id + ) att_path = os.path.join(settings.MEDIA_ROOT, path) - if settings.DEFAULT_FILE_STORAGE == "django.core.files.storage.FileSystemStorage": + if ( + settings.DEFAULT_FILE_STORAGE + == "django.core.files.storage.FileSystemStorage" + ): if not os.path.exists(att_path): os.makedirs(att_path, helpdesk_settings.HELPDESK_ATTACHMENT_DIR_PERMS) return os.path.join(path, filename) @@ -1249,34 +1315,40 @@ class PreSetReply(models.Model): When replying to a ticket, the user can select any reply set for the current queue, and the body text is fetched via AJAX. """ + class Meta: - ordering = ('name',) - verbose_name = _('Pre-set reply') - verbose_name_plural = _('Pre-set replies') + ordering = ("name",) + verbose_name = _("Pre-set reply") + verbose_name_plural = _("Pre-set replies") queues = models.ManyToManyField( Queue, blank=True, - help_text=_('Leave blank to allow this reply to be used for all ' - 'queues, or select those queues you wish to limit this reply to.'), + help_text=_( + "Leave blank to allow this reply to be used for all " + "queues, or select those queues you wish to limit this reply to." + ), ) name = models.CharField( - _('Name'), + _("Name"), max_length=100, - help_text=_('Only used to assist users with selecting a reply - not ' - 'shown to the user.'), + help_text=_( + "Only used to assist users with selecting a reply - not shown to the user." + ), ) body = models.TextField( - _('Body'), - help_text=_('Context available: {{ ticket }} - ticket object (eg ' - '{{ ticket.title }}); {{ queue }} - The queue; and {{ user }} ' - '- the current user.'), + _("Body"), + help_text=_( + "Context available: {{ ticket }} - ticket object (eg " + "{{ ticket.title }}); {{ queue }} - The queue; and {{ user }} " + "- the current user." + ), ) def __str__(self): - return '%s' % self.name + return "%s" % self.name class EscalationExclusion(models.Model): @@ -1293,26 +1365,28 @@ class EscalationExclusion(models.Model): queues = models.ManyToManyField( Queue, blank=True, - help_text=_('Leave blank for this exclusion to be applied to all queues, ' - 'or select those queues you wish to exclude with this entry.'), + help_text=_( + "Leave blank for this exclusion to be applied to all queues, " + "or select those queues you wish to exclude with this entry." + ), ) name = models.CharField( - _('Name'), + _("Name"), max_length=100, ) date = models.DateField( - _('Date'), - help_text=_('Date on which escalation should not happen'), + _("Date"), + help_text=_("Date on which escalation should not happen"), ) def __str__(self): - return '%s' % self.name + return "%s" % self.name class Meta: - verbose_name = _('Escalation exclusion') - verbose_name_plural = _('Escalation exclusions') + verbose_name = _("Escalation exclusion") + verbose_name_plural = _("Escalation exclusions") class EmailTemplate(models.Model): @@ -1325,54 +1399,59 @@ class EmailTemplate(models.Model): """ template_name = models.CharField( - _('Template Name'), + _("Template Name"), max_length=100, ) subject = models.CharField( - _('Subject'), + _("Subject"), max_length=100, - help_text=_('This will be prefixed with "[ticket.ticket] ticket.title"' - '. We recommend something simple such as "(Updated") or "(Closed)"' - ' - the same context is available as in plain_text, below.'), + help_text=_( + 'This will be prefixed with "[ticket.ticket] ticket.title"' + '. We recommend something simple such as "(Updated") or "(Closed)"' + " - the same context is available as in plain_text, below." + ), ) heading = models.CharField( - _('Heading'), + _("Heading"), max_length=100, - help_text=_('In HTML e-mails, this will be the heading at the top of ' - 'the email - the same context is available as in plain_text, ' - 'below.'), + help_text=_( + "In HTML e-mails, this will be the heading at the top of " + "the email - the same context is available as in plain_text, " + "below." + ), ) plain_text = models.TextField( - _('Plain Text'), - help_text=_('The context available to you includes {{ ticket }}, ' - '{{ queue }}, and depending on the time of the call: ' - '{{ resolution }} or {{ comment }}.'), + _("Plain Text"), + help_text=_( + "The context available to you includes {{ ticket }}, " + "{{ queue }}, and depending on the time of the call: " + "{{ resolution }} or {{ comment }}." + ), ) html = models.TextField( - _('HTML'), - help_text=_( - 'The same context is available here as in plain_text, above.'), + _("HTML"), + help_text=_("The same context is available here as in plain_text, above."), ) locale = models.CharField( - _('Locale'), + _("Locale"), max_length=10, blank=True, null=True, - help_text=_('Locale of this template.'), + help_text=_("Locale of this template."), ) def __str__(self): - return '%s' % self.template_name + return "%s" % self.template_name class Meta: - ordering = ('template_name', 'locale') - verbose_name = _('e-mail template') - verbose_name_plural = _('e-mail templates') + ordering = ("template_name", "locale") + verbose_name = _("e-mail template") + verbose_name_plural = _("e-mail templates") class KBCategory(models.Model): @@ -1382,21 +1461,21 @@ class KBCategory(models.Model): """ name = models.CharField( - _('Name of the category'), + _("Name of the category"), max_length=100, ) title = models.CharField( - _('Title on knowledgebase page'), + _("Title on knowledgebase page"), max_length=100, ) slug = models.SlugField( - _('Slug'), + _("Slug"), ) description = models.TextField( - _('Description'), + _("Description"), ) queue = models.ForeignKey( @@ -1405,25 +1484,26 @@ class KBCategory(models.Model): null=True, on_delete=models.CASCADE, verbose_name=_( - 'Default queue when creating a ticket after viewing this category.'), + "Default queue when creating a ticket after viewing this category." + ), ) public = models.BooleanField( - default=True, - verbose_name=_("Is KBCategory publicly visible?") + default=True, verbose_name=_("Is KBCategory publicly visible?") ) def __str__(self): - return '%s' % self.name + return "%s" % self.name class Meta: - ordering = ('title',) - verbose_name = _('Knowledge base category') - verbose_name_plural = _('Knowledge base categories') + ordering = ("title",) + verbose_name = _("Knowledge base category") + verbose_name_plural = _("Knowledge base categories") def get_absolute_url(self): from django.urls import reverse - return reverse('helpdesk:kb_category', kwargs={'slug': self.slug}) + + return reverse("helpdesk:kb_category", kwargs={"slug": self.slug}) class KBItem(models.Model): @@ -1431,68 +1511,68 @@ class KBItem(models.Model): An item within the knowledgebase. Very straightforward question/answer style system. """ + voted_by = models.ManyToManyField( settings.AUTH_USER_MODEL, - related_name='votes', + related_name="votes", ) downvoted_by = models.ManyToManyField( settings.AUTH_USER_MODEL, - related_name='downvotes', + related_name="downvotes", ) category = models.ForeignKey( KBCategory, on_delete=models.CASCADE, - verbose_name=_('Category'), + verbose_name=_("Category"), ) title = models.CharField( - _('Title'), + _("Title"), max_length=100, ) question = models.TextField( - _('Question'), + _("Question"), ) answer = models.TextField( - _('Answer'), + _("Answer"), ) votes = models.IntegerField( - _('Votes'), - help_text=_('Total number of votes cast for this item'), + _("Votes"), + help_text=_("Total number of votes cast for this item"), default=0, ) recommendations = models.IntegerField( - _('Positive Votes'), - help_text=_('Number of votes for this item which were POSITIVE.'), + _("Positive Votes"), + help_text=_("Number of votes for this item which were POSITIVE."), default=0, ) last_updated = models.DateTimeField( - _('Last Updated'), - help_text=_( - 'The date on which this question was most recently changed.'), + _("Last Updated"), + help_text=_("The date on which this question was most recently changed."), blank=True, ) team = models.ForeignKey( helpdesk_settings.HELPDESK_TEAMS_MODEL, on_delete=models.CASCADE, - verbose_name=_('Team'), + verbose_name=_("Team"), blank=True, null=True, ) order = models.PositiveIntegerField( - _('Order'), + _("Order"), blank=True, null=True, ) enabled = models.BooleanField( - _('Enabled to display to users'), + _("Enabled to display to users"), default=True, ) @@ -1505,34 +1585,46 @@ class KBItem(models.Model): return helpdesk_settings.HELPDESK_KBITEM_TEAM_GETTER(self) def _score(self): - """ Return a score out of 10 or Unrated if no votes """ + """Return a score out of 10 or Unrated if no votes""" if self.votes > 0: return (self.recommendations / self.votes) * 10 else: - return _('Unrated') + return _("Unrated") + score = property(_score) def __str__(self): - return '%s: %s' % (self.category.title, self.title) + return "%s: %s" % (self.category.title, self.title) class Meta: - ordering = ('order', 'title',) - verbose_name = _('Knowledge base item') - verbose_name_plural = _('Knowledge base items') + ordering = ( + "order", + "title", + ) + verbose_name = _("Knowledge base item") + verbose_name_plural = _("Knowledge base items") def get_absolute_url(self): from django.urls import reverse - return str(reverse('helpdesk:kb_category', args=(self.category.slug,))) + "?kbitem=" + str(self.pk) + + return ( + str(reverse("helpdesk:kb_category", args=(self.category.slug,))) + + "?kbitem=" + + str(self.pk) + ) def query_url(self): from django.urls import reverse - return str(reverse('helpdesk:list')) + "?kbitem=" + str(self.pk) + + return str(reverse("helpdesk:list")) + "?kbitem=" + str(self.pk) def num_open_tickets(self): return Ticket.objects.filter(kbitem=self, status__in=(1, 2)).count() def unassigned_tickets(self): - return Ticket.objects.filter(kbitem=self, status__in=(1, 2), assigned_to__isnull=True) + return Ticket.objects.filter( + kbitem=self, status__in=(1, 2), assigned_to__isnull=True + ) def get_markdown(self): return get_markdown(self.answer) @@ -1549,64 +1641,66 @@ class SavedSearch(models.Model): * All tickets containing the word 'billing'. etc... """ + user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, - verbose_name=_('User'), + verbose_name=_("User"), ) title = models.CharField( - _('Query Name'), + _("Query Name"), max_length=100, - help_text=_('User-provided name for this query'), + help_text=_("User-provided name for this query"), ) shared = models.BooleanField( - _('Shared With Other Users?'), + _("Shared With Other Users?"), blank=True, default=False, - help_text=_('Should other users see this query?'), + help_text=_("Should other users see this query?"), ) query = models.TextField( - _('Search Query'), - help_text=_('Pickled query object. Be wary changing this.'), + _("Search Query"), + help_text=_("Pickled query object. Be wary changing this."), ) def __str__(self): if self.shared: - return '%s (*)' % self.title + return "%s (*)" % self.title else: - return '%s' % self.title + return "%s" % self.title class Meta: - verbose_name = _('Saved search') - verbose_name_plural = _('Saved searches') + verbose_name = _("Saved search") + verbose_name_plural = _("Saved searches") def get_default_setting(setting): from helpdesk.settings import DEFAULT_USER_SETTINGS + return DEFAULT_USER_SETTINGS[setting] def login_view_ticketlist_default(): - return get_default_setting('login_view_ticketlist') + return get_default_setting("login_view_ticketlist") def email_on_ticket_change_default(): - return get_default_setting('email_on_ticket_change') + return get_default_setting("email_on_ticket_change") def email_on_ticket_assign_default(): - return get_default_setting('email_on_ticket_assign') + return get_default_setting("email_on_ticket_assign") def tickets_per_page_default(): - return get_default_setting('tickets_per_page') + return get_default_setting("tickets_per_page") def use_email_as_submitter_default(): - return get_default_setting('use_email_as_submitter') + return get_default_setting("use_email_as_submitter") class UserSettings(models.Model): @@ -1615,67 +1709,74 @@ class UserSettings(models.Model): as notification preferences and other things that should probably be configurable. """ - PAGE_SIZES = ((10, '10'), (25, '25'), (50, '50'), (100, '100')) + + PAGE_SIZES = ((10, "10"), (25, "25"), (50, "50"), (100, "100")) user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, - related_name="usersettings_helpdesk") + related_name="usersettings_helpdesk", + ) settings_pickled = models.TextField( - _('DEPRECATED! Settings Dictionary DEPRECATED!'), - help_text=_('DEPRECATED! This is a base64-encoded representation of a pickled Python dictionary. ' - 'Do not change this field via the admin.'), + _("DEPRECATED! Settings Dictionary DEPRECATED!"), + help_text=_( + "DEPRECATED! This is a base64-encoded representation of a pickled Python dictionary. " + "Do not change this field via the admin." + ), blank=True, null=True, ) login_view_ticketlist = models.BooleanField( - verbose_name=_('Show Ticket List on Login?'), + verbose_name=_("Show Ticket List on Login?"), help_text=_( - 'Display the ticket list upon login? Otherwise, the dashboard is shown.'), + "Display the ticket list upon login? Otherwise, the dashboard is shown." + ), default=login_view_ticketlist_default, ) email_on_ticket_change = models.BooleanField( - verbose_name=_('E-mail me on ticket change?'), + verbose_name=_("E-mail me on ticket change?"), help_text=_( - 'If you\'re the ticket owner and the ticket is changed via the web by somebody else,' - 'do you want to receive an e-mail?' + "If you're the ticket owner and the ticket is changed via the web by somebody else," + "do you want to receive an e-mail?" ), default=email_on_ticket_change_default, ) email_on_ticket_assign = models.BooleanField( - verbose_name=_('E-mail me when assigned a ticket?'), + 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?'), + "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?'), + verbose_name=_("Number of tickets to show per page"), + help_text=_("How many tickets do you want to see on the Ticket List page?"), default=tickets_per_page_default, choices=PAGE_SIZES, ) use_email_as_submitter = models.BooleanField( - verbose_name=_('Use my e-mail address when submitting tickets?'), - help_text=_('When you submit a ticket, do you want to automatically ' - 'use your e-mail address as the submitter address? You ' - 'can type a different e-mail address when entering the ' - 'ticket if needed, this option only changes the default.'), + verbose_name=_("Use my e-mail address when submitting tickets?"), + help_text=_( + "When you submit a ticket, do you want to automatically " + "use your e-mail address as the submitter address? You " + "can type a different e-mail address when entering the " + "ticket if needed, this option only changes the default." + ), default=use_email_as_submitter_default, ) def __str__(self): - return 'Preferences for %s' % self.user + return "Preferences for %s" % self.user class Meta: - verbose_name = _('User Setting') - verbose_name_plural = _('User Settings') + verbose_name = _("User Setting") + verbose_name_plural = _("User Settings") def create_usersettings(sender, instance, created, **kwargs): @@ -1691,8 +1792,7 @@ 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): @@ -1701,46 +1801,53 @@ class IgnoreEmail(models.Model): processing IMAP and POP3 mailboxes, eg mails from postmaster or from known trouble-makers. """ + class Meta: - verbose_name = _('Ignored e-mail address') - verbose_name_plural = _('Ignored e-mail addresses') + verbose_name = _("Ignored e-mail address") + verbose_name_plural = _("Ignored e-mail addresses") queues = models.ManyToManyField( Queue, blank=True, - help_text=_('Leave blank for this e-mail to be ignored on all queues, ' - 'or select those queues you wish to ignore this e-mail for.'), + help_text=_( + "Leave blank for this e-mail to be ignored on all queues, " + "or select those queues you wish to ignore this e-mail for." + ), ) name = models.CharField( - _('Name'), + _("Name"), max_length=100, ) date = models.DateField( - _('Date'), - help_text=_('Date on which this e-mail address was added'), + _("Date"), + help_text=_("Date on which this e-mail address was added"), blank=True, - editable=False + editable=False, ) email_address = models.CharField( - _('E-Mail Address'), + _("E-Mail Address"), max_length=150, - help_text=_('Enter a full e-mail address, or portions with ' - 'wildcards, eg *@domain.com or postmaster@*.'), + help_text=_( + "Enter a full e-mail address, or portions with " + "wildcards, eg *@domain.com or postmaster@*." + ), ) keep_in_mailbox = models.BooleanField( - _('Save Emails in Mailbox?'), + _("Save Emails in Mailbox?"), blank=True, default=False, - help_text=_('Do you want to save emails from this address in the mailbox? ' - 'If this is unticked, emails from this address will be deleted.'), + help_text=_( + "Do you want to save emails from this address in the mailbox? " + "If this is unticked, emails from this address will be deleted." + ), ) def __str__(self): - return '%s' % self.name + return "%s" % self.name def save(self, *args, **kwargs): if not self.date: @@ -1751,11 +1858,11 @@ class IgnoreEmail(models.Model): """Return a list of the queues this IgnoreEmail applies to. If this IgnoreEmail applies to ALL queues, return '*'. """ - queues = self.queues.all().order_by('title') + queues = self.queues.all().order_by("title") if len(queues) == 0: - return '*' + return "*" else: - return ', '.join([str(q) for q in queues]) + return ", ".join([str(q) for q in queues]) def test(self, email): """ @@ -1772,10 +1879,15 @@ class IgnoreEmail(models.Model): own_parts = self.email_address.split("@") email_parts = email.split("@") - if self.email_address == email or \ - own_parts[0] == "*" and own_parts[1] == email_parts[1] or \ - own_parts[1] == "*" and own_parts[0] == email_parts[0] or \ - own_parts[0] == "*" and own_parts[1] == "*": + if ( + self.email_address == email + or own_parts[0] == "*" + and own_parts[1] == email_parts[1] + or own_parts[1] == "*" + and own_parts[0] == email_parts[0] + or own_parts[0] == "*" + and own_parts[1] == "*" + ): return True else: return False @@ -1794,7 +1906,7 @@ class TicketCC(models.Model): ticket = models.ForeignKey( Ticket, on_delete=models.CASCADE, - verbose_name=_('Ticket'), + verbose_name=_("Ticket"), ) user = models.ForeignKey( @@ -1802,29 +1914,29 @@ class TicketCC(models.Model): on_delete=models.CASCADE, blank=True, null=True, - help_text=_('User who wishes to receive updates for this ticket.'), - verbose_name=_('User'), + help_text=_("User who wishes to receive updates for this ticket."), + verbose_name=_("User"), ) email = models.EmailField( - _('E-Mail Address'), + _("E-Mail Address"), blank=True, null=True, - help_text=_('For non-user followers, enter their e-mail address'), + help_text=_("For non-user followers, enter their e-mail address"), ) can_view = models.BooleanField( - _('Can View Ticket?'), + _("Can View Ticket?"), blank=True, default=False, - help_text=_('Can this CC login to view the ticket details?'), + help_text=_("Can this CC login to view the ticket details?"), ) can_update = models.BooleanField( - _('Can Update Ticket?'), + _("Can Update Ticket?"), blank=True, default=False, - help_text=_('Can this CC login and update the ticket?'), + help_text=_("Can this CC login and update the ticket?"), ) def _email_address(self): @@ -1832,6 +1944,7 @@ class TicketCC(models.Model): return self.user.email else: return self.email + email_address = property(_email_address) def _display(self): @@ -1839,20 +1952,20 @@ class TicketCC(models.Model): return self.user else: return self.email + display = property(_display) def __str__(self): - return '%s for %s' % (self.display, self.ticket.title) + return "%s for %s" % (self.display, self.ticket.title) def clean(self): if self.user and not self.user.email: - raise ValidationError('User has no email address') + raise ValidationError("User has no email address") class CustomFieldManager(models.Manager): - def get_queryset(self): - return super(CustomFieldManager, self).get_queryset().order_by('ordering') + return super(CustomFieldManager, self).get_queryset().order_by("ordering") class CustomField(models.Model): @@ -1861,153 +1974,160 @@ class CustomField(models.Model): """ name = models.SlugField( - _('Field Name'), - help_text=_('As used in the database and behind the scenes. ' - 'Must be unique and consist of only lowercase letters with no punctuation.'), + _("Field Name"), + help_text=_( + "As used in the database and behind the scenes. " + "Must be unique and consist of only lowercase letters with no punctuation." + ), unique=True, ) label = models.CharField( - _('Label'), + _("Label"), max_length=30, - help_text=_('The display label for this field'), + help_text=_("The display label for this field"), ) help_text = models.TextField( - _('Help Text'), - help_text=_('Shown to the user when editing the ticket'), + _("Help Text"), + help_text=_("Shown to the user when editing the ticket"), blank=True, - null=True + null=True, ) DATA_TYPE_CHOICES = ( - ('varchar', _('Character (single line)')), - ('text', _('Text (multi-line)')), - ('integer', _('Integer')), - ('decimal', _('Decimal')), - ('list', _('List')), - ('boolean', _('Boolean (checkbox yes/no)')), - ('date', _('Date')), - ('time', _('Time')), - ('datetime', _('Date & Time')), - ('email', _('E-Mail Address')), - ('url', _('URL')), - ('ipaddress', _('IP Address')), - ('slug', _('Slug')), + ("varchar", _("Character (single line)")), + ("text", _("Text (multi-line)")), + ("integer", _("Integer")), + ("decimal", _("Decimal")), + ("list", _("List")), + ("boolean", _("Boolean (checkbox yes/no)")), + ("date", _("Date")), + ("time", _("Time")), + ("datetime", _("Date & Time")), + ("email", _("E-Mail Address")), + ("url", _("URL")), + ("ipaddress", _("IP Address")), + ("slug", _("Slug")), ) data_type = models.CharField( - _('Data Type'), + _("Data Type"), max_length=100, - help_text=_('Allows you to restrict the data entered into this field'), + help_text=_("Allows you to restrict the data entered into this field"), choices=DATA_TYPE_CHOICES, ) max_length = models.IntegerField( - _('Maximum Length (characters)'), + _("Maximum Length (characters)"), blank=True, null=True, ) decimal_places = models.IntegerField( - _('Decimal Places'), - help_text=_('Only used for decimal fields'), + _("Decimal Places"), + help_text=_("Only used for decimal fields"), blank=True, null=True, ) empty_selection_list = models.BooleanField( - _('Add empty first choice to List?'), + _("Add empty first choice to List?"), default=False, - help_text=_('Only for List: adds an empty first entry to the choices list, ' - 'which enforces that the user makes an active choice.'), + help_text=_( + "Only for List: adds an empty first entry to the choices list, " + "which enforces that the user makes an active choice." + ), ) list_values = models.TextField( - _('List Values'), - help_text=_('For list fields only. Enter one option per line.'), + _("List Values"), + help_text=_("For list fields only. Enter one option per line."), blank=True, null=True, ) ordering = models.IntegerField( - _('Ordering'), + _("Ordering"), help_text=_( - 'Lower numbers are displayed first; higher numbers are listed later'), + "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) required = models.BooleanField( - _('Required?'), - help_text=_('Does the user have to enter a value for this field?'), + _("Required?"), + help_text=_("Does the user have to enter a value for this field?"), default=False, ) staff_only = models.BooleanField( - _('Staff Only?'), - help_text=_('If this is ticked, then the public submission form ' - 'will NOT show this field'), + _("Staff Only?"), + help_text=_( + "If this is ticked, then the public submission form " + "will NOT show this field" + ), default=False, ) objects = CustomFieldManager() def __str__(self): - return '%s' % self.name + return "%s" % self.name class Meta: - verbose_name = _('Custom field') - verbose_name_plural = _('Custom fields') + verbose_name = _("Custom field") + verbose_name_plural = _("Custom fields") def get_choices(self): - if not self.data_type == 'list': + if not self.data_type == "list": return None choices = self.choices_as_array if self.empty_selection_list: - choices.insert(0, ('', '---------')) + choices.insert(0, ("", "---------")) return choices def build_api_field(self): customfield_to_api_field_dict = { - 'varchar': serializers.CharField, - 'text': serializers.CharField, - 'integer': serializers.IntegerField, - 'decimal': serializers.DecimalField, - 'list': serializers.ChoiceField, - 'boolean': serializers.BooleanField, - 'date': serializers.DateField, - 'time': serializers.TimeField, - 'datetime': serializers.DateTimeField, - 'email': serializers.EmailField, - 'url': serializers.URLField, - 'ipaddress': serializers.IPAddressField, - 'slug': serializers.SlugField, + "varchar": serializers.CharField, + "text": serializers.CharField, + "integer": serializers.IntegerField, + "decimal": serializers.DecimalField, + "list": serializers.ChoiceField, + "boolean": serializers.BooleanField, + "date": serializers.DateField, + "time": serializers.TimeField, + "datetime": serializers.DateTimeField, + "email": serializers.EmailField, + "url": serializers.URLField, + "ipaddress": serializers.IPAddressField, + "slug": serializers.SlugField, } # 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': - attributes['style'] = {'base_template': 'textarea.html'} - elif self.data_type == 'decimal': - attributes['decimal_places'] = self.decimal_places - attributes['max_digits'] = self.max_length - elif self.data_type == 'list': - attributes['choices'] = self.get_choices() + if self.data_type in ("varchar", "text"): + attributes["max_length"] = self.max_length + if self.data_type == "text": + attributes["style"] = {"base_template": "textarea.html"} + elif self.data_type == "decimal": + attributes["decimal_places"] = self.decimal_places + attributes["max_digits"] = self.max_length + elif self.data_type == "list": + attributes["choices"] = self.get_choices() try: return customfield_to_api_field_dict[self.data_type](**attributes) @@ -2019,28 +2139,28 @@ class TicketCustomFieldValue(models.Model): ticket = models.ForeignKey( Ticket, on_delete=models.CASCADE, - verbose_name=_('Ticket'), + verbose_name=_("Ticket"), ) field = models.ForeignKey( CustomField, on_delete=models.CASCADE, - verbose_name=_('Field'), + verbose_name=_("Field"), ) value = models.TextField(blank=True, null=True) def __str__(self): - return '%s / %s' % (self.ticket, self.field) + return "%s / %s" % (self.ticket, self.field) @property def default_value(self) -> str: return _("Not defined") class Meta: - unique_together = (('ticket', 'field'),) - verbose_name = _('Ticket custom field value') - verbose_name_plural = _('Ticket custom field values') + unique_together = (("ticket", "field"),) + verbose_name = _("Ticket custom field value") + verbose_name_plural = _("Ticket custom field values") class TicketDependency(models.Model): @@ -2049,49 +2169,49 @@ class TicketDependency(models.Model): To help enforce this, a helper function `can_be_resolved` on each Ticket instance checks that these have all been resolved. """ + class Meta: - unique_together = (('ticket', 'depends_on'),) - verbose_name = _('Ticket dependency') - verbose_name_plural = _('Ticket dependencies') + unique_together = (("ticket", "depends_on"),) + verbose_name = _("Ticket dependency") + verbose_name_plural = _("Ticket dependencies") ticket = models.ForeignKey( Ticket, on_delete=models.CASCADE, - verbose_name=_('Ticket'), - related_name='ticketdependency', + verbose_name=_("Ticket"), + related_name="ticketdependency", ) depends_on = models.ForeignKey( Ticket, on_delete=models.CASCADE, - verbose_name=_('Depends On Ticket'), - related_name='depends_on', + verbose_name=_("Depends On Ticket"), + related_name="depends_on", ) def __str__(self): - return '%s / %s' % (self.ticket, self.depends_on) + return "%s / %s" % (self.ticket, self.depends_on) def is_a_list_without_empty_element(task_list): if not isinstance(task_list, list): - raise ValidationError(f'{task_list} is not a list') + raise ValidationError(f"{task_list} is not a list") for task in task_list: if not isinstance(task, str): - raise ValidationError(f'{task} is not a string') - if task.strip() == '': - raise ValidationError('A task cannot be an empty string') + raise ValidationError(f"{task} is not a string") + if task.strip() == "": + raise ValidationError("A task cannot be an empty string") class ChecklistTemplate(models.Model): - name = models.CharField( - verbose_name=_('Name'), - max_length=100 + name = models.CharField(verbose_name=_("Name"), max_length=100) + task_list = models.JSONField( + verbose_name=_("Task List"), validators=[is_a_list_without_empty_element] ) - task_list = models.JSONField(verbose_name=_('Task List'), validators=[is_a_list_without_empty_element]) class Meta: - verbose_name = _('Checklist Template') - verbose_name_plural = _('Checklist Templates') + verbose_name = _("Checklist Template") + verbose_name_plural = _("Checklist Templates") def __str__(self): return self.name @@ -2101,17 +2221,14 @@ class Checklist(models.Model): ticket = models.ForeignKey( Ticket, on_delete=models.CASCADE, - verbose_name=_('Ticket'), - related_name='checklists', - ) - name = models.CharField( - verbose_name=_('Name'), - max_length=100 + verbose_name=_("Ticket"), + related_name="checklists", ) + name = models.CharField(verbose_name=_("Name"), max_length=100) class Meta: - verbose_name = _('Checklist') - verbose_name_plural = _('Checklists') + verbose_name = _("Checklist") + verbose_name_plural = _("Checklists") def __str__(self): return self.name @@ -2133,29 +2250,23 @@ class ChecklistTask(models.Model): checklist = models.ForeignKey( Checklist, on_delete=models.CASCADE, - verbose_name=_('Checklist'), - related_name='tasks', - ) - description = models.CharField( - verbose_name=_('Description'), - max_length=250 + verbose_name=_("Checklist"), + related_name="tasks", ) + description = models.CharField(verbose_name=_("Description"), max_length=250) completion_date = models.DateTimeField( - verbose_name=_('Completion Date'), - null=True, - blank=True + verbose_name=_("Completion Date"), null=True, blank=True ) position = models.PositiveSmallIntegerField( - verbose_name=_('Position'), - db_index=True + verbose_name=_("Position"), db_index=True ) objects = ChecklistTaskQuerySet.as_manager() class Meta: - verbose_name = _('Checklist Task') - verbose_name_plural = _('Checklist Tasks') - ordering = ('position',) + verbose_name = _("Checklist Task") + verbose_name_plural = _("Checklist Tasks") + ordering = ("position",) def __str__(self): return self.description diff --git a/helpdesk/query.py b/helpdesk/query.py index ea69a133..af205739 100644 --- a/helpdesk/query.py +++ b/helpdesk/query.py @@ -1,4 +1,3 @@ - from base64 import b64decode, b64encode from django.db.models import Q, Max from django.db.models import F, Window, Subquery, OuterRef @@ -15,61 +14,61 @@ def query_to_base64(query): """ Converts a query dict object to a base64-encoded bytes object. """ - return b64encode(json.dumps(query).encode('UTF-8')).decode("ascii") + return b64encode(json.dumps(query).encode("UTF-8")).decode("ascii") def query_from_base64(b64data): """ Converts base64-encoded bytes object back to a query dict object. """ - query = {'search_string': ''} - query.update(json.loads(b64decode(b64data).decode('utf-8'))) - if query['search_string'] is None: - query['search_string'] = '' + query = {"search_string": ""} + query.update(json.loads(b64decode(b64data).decode("utf-8"))) + if query["search_string"] is None: + query["search_string"] = "" return query def get_search_filter_args(search): if not search: return Q() - if search.startswith('queue:'): - return Q(queue__title__icontains=search[len('queue:'):]) - if search.startswith('priority:'): - return Q(priority__icontains=search[len('priority:'):]) + if search.startswith("queue:"): + return Q(queue__title__icontains=search[len("queue:") :]) + if search.startswith("priority:"): + return Q(priority__icontains=search[len("priority:") :]) my_filter = Q() for subsearch in search.split("OR"): subsearch = subsearch.strip() if not subsearch: continue my_filter = ( - filter | - Q(id__icontains=subsearch) | - Q(title__icontains=subsearch) | - Q(description__icontains=subsearch) | - Q(priority__icontains=subsearch) | - Q(resolution__icontains=subsearch) | - Q(submitter_email__icontains=subsearch) | - Q(assigned_to__email__icontains=subsearch) | - Q(ticketcustomfieldvalue__value__icontains=subsearch) | - Q(created__icontains=subsearch) | - Q(due_date__icontains=subsearch) + filter + | Q(id__icontains=subsearch) + | Q(title__icontains=subsearch) + | Q(description__icontains=subsearch) + | Q(priority__icontains=subsearch) + | Q(resolution__icontains=subsearch) + | Q(submitter_email__icontains=subsearch) + | Q(assigned_to__email__icontains=subsearch) + | Q(ticketcustomfieldvalue__value__icontains=subsearch) + | Q(created__icontains=subsearch) + | Q(due_date__icontains=subsearch) ) return my_filter DATATABLES_ORDER_COLUMN_CHOICES = Choices( - ('0', 'id'), - ('1', 'title'), - ('2', 'priority'), - ('3', 'queue'), - ('4', 'status'), - ('5', 'created'), - ('6', 'due_date'), - ('7', 'assigned_to'), - ('8', 'submitter_email'), - ('9', 'last_followup'), + ("0", "id"), + ("1", "title"), + ("2", "priority"), + ("3", "queue"), + ("4", "status"), + ("5", "created"), + ("6", "due_date"), + ("7", "assigned_to"), + ("8", "submitter_email"), + ("9", "last_followup"), # ('10', 'time_spent'), - ('11', 'kbitem'), + ("11", "kbitem"), ) @@ -78,22 +77,19 @@ def get_query_class(): def _get_query_class(): return __Query__ - return getattr(settings, - 'HELPDESK_QUERY_CLASS', - _get_query_class)() + + return getattr(settings, "HELPDESK_QUERY_CLASS", _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): - search = self.params.get('search_string', '') + search = self.params.get("search_string", "") return get_search_filter_args(search) def __run__(self, queryset): @@ -112,15 +108,15 @@ class __Query__: sorting: The name of the column to sort by """ q_args = [] - value_filters = self.params.get('filtering', {}) - null_filters = self.params.get('filtering_null', {}) + value_filters = self.params.get("filtering", {}) + null_filters = self.params.get("filtering_null", {}) if null_filters: if value_filters: # Check if any of the value value_filters are for the same field as the # ISNULL filter so that an OR filter can be set up matched_null_keys = [] for null_key in null_filters: - field_path = null_key[:-8] # Chop off the "__isnull" + field_path = null_key[:-8] # Chop off the "__isnull" matched_key = None for val_key in value_filters: if val_key.startswith(field_path): @@ -140,10 +136,12 @@ class __Query__: for null_key in matched_null_keys: del null_filters[null_key] queryset = queryset.filter( - *q_args, (Q(**value_filters) & Q(**null_filters)) & self.get_search_filter_args()) - sorting = self.params.get('sorting', None) + *q_args, + (Q(**value_filters) & Q(**null_filters)) & self.get_search_filter_args(), + ) + sorting = self.params.get("sorting", None) if sorting: - sortreverse = self.params.get('sortreverse', None) + sortreverse = self.params.get("sortreverse", None) if sortreverse: sorting = "-%s" % sorting queryset = queryset.order_by(sorting) @@ -165,45 +163,49 @@ class __Query__: to a Serializer called DatatablesTicketSerializer in serializers.py. """ objects = self.get() - order_by = '-created' - draw = int(kwargs.get('draw', [0])[0]) - length = int(kwargs.get('length', [25])[0]) - start = int(kwargs.get('start', [0])[0]) - search_value = kwargs.get('search[value]', [""])[0] - order_column = kwargs.get('order[0][column]', ['5'])[0] - order = kwargs.get('order[0][dir]', ["asc"])[0] - + order_by = "-created" + draw = int(kwargs.get("draw", [0])[0]) + length = int(kwargs.get("length", [25])[0]) + start = int(kwargs.get("start", [0])[0]) + search_value = kwargs.get("search[value]", [""])[0] + order_column = kwargs.get("order[0][column]", ["5"])[0] + order = kwargs.get("order[0][dir]", ["asc"])[0] + order_column = DATATABLES_ORDER_COLUMN_CHOICES[order_column] # django orm '-' -> desc - if order == 'desc': - order_column = '-' + order_column - + if order == "desc": + order_column = "-" + order_column + queryset = objects.annotate( last_followup=Subquery( - FollowUp.objects.order_by().annotate( + FollowUp.objects.order_by() + .annotate( last_followup=Window( expression=Max("date"), - partition_by=[F("ticket_id"),], - order_by="-date" + partition_by=[ + F("ticket_id"), + ], + order_by="-date", ) - ).filter( - ticket_id=OuterRef("id") - ).values("last_followup").distinct() + ) + .filter(ticket_id=OuterRef("id")) + .values("last_followup") + .distinct() ) ).order_by(order_by) - + total = queryset.count() if search_value: # Dead code currently queryset = queryset.filter(get_search_filter_args(search_value)) count = queryset.count() - queryset = queryset.order_by(order_column)[start:start + length] + queryset = queryset.order_by(order_column)[start : start + length] return { - 'data': DatatablesTicketSerializer(queryset, many=True).data, - 'recordsFiltered': count, - 'recordsTotal': total, - 'draw': draw + "data": DatatablesTicketSerializer(queryset, many=True).data, + "recordsFiltered": count, + "recordsTotal": total, + "draw": draw, } def get_timeline_context(self): @@ -212,33 +214,38 @@ class __Query__: for ticket in self.get(): for followup in ticket.followup_set.all(): event = { - 'start_date': self.mk_timeline_date(followup.date), - 'text': { - 'headline': ticket.title + ' - ' + followup.title, - 'text': ( - (escape(followup.comment) - if followup.comment else _('No text')) - + - '
%s' - % - (reverse('helpdesk:view', kwargs={ - 'ticket_id': ticket.pk}), _("View ticket")) + "start_date": self.mk_timeline_date(followup.date), + "text": { + "headline": ticket.title + " - " + followup.title, + "text": ( + ( + escape(followup.comment) + if followup.comment + else _("No text") + ) + + '
%s' + % ( + reverse( + "helpdesk:view", kwargs={"ticket_id": ticket.pk} + ), + _("View ticket"), + ) ), }, - 'group': _('Messages'), + "group": _("Messages"), } events.append(event) return { - 'events': events, + "events": events, } def mk_timeline_date(self, date): return { - 'year': date.year, - 'month': date.month, - 'day': date.day, - 'hour': date.hour, - 'minute': date.minute, - 'second': date.second, + "year": date.year, + "month": date.month, + "day": date.day, + "hour": date.hour, + "minute": date.minute, + "second": date.second, } diff --git a/helpdesk/serializers.py b/helpdesk/serializers.py index 142e6689..9d25b7de 100644 --- a/helpdesk/serializers.py +++ b/helpdesk/serializers.py @@ -15,6 +15,7 @@ class DatatablesTicketSerializer(serializers.ModelSerializer): A serializer for the Ticket model, returns data in the format as required by datatables for ticket_list.html. Called from staff.datatables_ticket_list. """ + ticket = serializers.SerializerMethodField() assigned_to = serializers.SerializerMethodField() submitter = serializers.SerializerMethodField() @@ -30,9 +31,22 @@ class DatatablesTicketSerializer(serializers.ModelSerializer): class Meta: model = Ticket # fields = '__all__' - fields = ('ticket', 'id', 'priority', 'title', 'queue', 'status', - 'created', 'due_date', 'assigned_to', 'submitter', 'last_followup', - 'row_class', 'time_spent', 'kbitem') + fields = ( + "ticket", + "id", + "priority", + "title", + "queue", + "status", + "created", + "due_date", + "assigned_to", + "submitter", + "last_followup", + "row_class", + "time_spent", + "kbitem", + ) def get_queue(self, obj): return {"title": obj.queue.title, "id": obj.queue.id} @@ -71,39 +85,46 @@ class DatatablesTicketSerializer(serializers.ModelSerializer): def get_kbitem(self, obj): return obj.kbitem.title if obj.kbitem else "" - + def get_last_followup(self, obj): return obj.last_followup - - + + class FollowUpAttachmentSerializer(serializers.ModelSerializer): class Meta: model = FollowUpAttachment - fields = ('id', 'followup', 'file', 'filename', 'mime_type', 'size') + fields = ("id", "followup", "file", "filename", "mime_type", "size") 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, - required=False + child=serializers.FileField(), write_only=True, required=False ) date = serializers.DateTimeField(read_only=True) class Meta: model = FollowUp fields = ( - 'id', 'ticket', 'user', 'title', 'comment', 'public', 'new_status', - 'time_spent', 'attachments', 'followupattachment_set', 'date', 'message_id', + "id", + "ticket", + "user", + "title", + "comment", + "public", + "new_status", + "time_spent", + "attachments", + "followupattachment_set", + "date", + "message_id", ) def create(self, validated_data): if validated_data["user"]: user = validated_data["user"] else: - user = self.context['request'].user + user = self.context["request"].user return update_ticket( user=user, ticket=validated_data["ticket"], @@ -121,12 +142,12 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() - fields = ('first_name', 'last_name', 'username', 'email', 'password') + fields = ("first_name", "last_name", "username", "email", "password") def create(self, validated_data): user = super(UserSerializer, self).create(validated_data) user.is_active = True - user.set_password(validated_data['password']) + user.set_password(validated_data["password"]) user.save() return user @@ -137,13 +158,14 @@ class BaseTicketSerializer(serializers.ModelSerializer): # Add custom fields for field in CustomField.objects.all(): - self.fields['custom_%s' % field.name] = field.build_api_field() + self.fields["custom_%s" % field.name] = field.build_api_field() class PublicTicketListingSerializer(BaseTicketSerializer): """ A serializer to be used by the public API for listing tickets. Don't expose private fields here! """ + ticket = serializers.SerializerMethodField() submitter = serializers.SerializerMethodField() created = serializers.SerializerMethodField() @@ -156,8 +178,18 @@ class PublicTicketListingSerializer(BaseTicketSerializer): class Meta: model = Ticket # fields = '__all__' - fields = ('ticket', 'id', 'title', 'queue', 'status', - 'created', 'due_date', 'submitter', 'kbitem', 'secret_key') + fields = ( + "ticket", + "id", + "title", + "queue", + "status", + "created", + "due_date", + "submitter", + "kbitem", + "secret_key", + ) def get_queue(self, obj): return {"title": obj.queue.title, "id": obj.queue.id} @@ -188,29 +220,40 @@ class TicketSerializer(BaseTicketSerializer): class Meta: model = Ticket fields = ( - 'id', 'queue', 'title', 'description', 'resolution', 'submitter_email', 'assigned_to', 'status', 'on_hold', - 'priority', 'due_date', 'merged_to', 'attachment', 'followup_set' + "id", + "queue", + "title", + "description", + "resolution", + "submitter_email", + "assigned_to", + "status", + "on_hold", + "priority", + "due_date", + "merged_to", + "attachment", + "followup_set", ) def create(self, validated_data): - """ Use TicketForm to validate and create ticket """ - queues = HelpdeskUser(self.context['request'].user).get_queues() + """Use TicketForm to validate and create ticket""" + queues = HelpdeskUser(self.context["request"].user).get_queues() queue_choices = [(q.id, q.title) for q in queues] data = validated_data.copy() - data['body'] = data['description'] + data["body"] = data["description"] # TicketForm needs id for ForeignKey (not the instance themselves) - data['queue'] = data['queue'].id - if data.get('assigned_to'): - data['assigned_to'] = data['assigned_to'].id - if data.get('merged_to'): - data['merged_to'] = data['merged_to'].id + data["queue"] = data["queue"].id + if data.get("assigned_to"): + data["assigned_to"] = data["assigned_to"].id + if data.get("merged_to"): + data["merged_to"] = data["merged_to"].id - files = {'attachment': data.pop('attachment', None)} + 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 = ticket_form.save(user=self.context["request"].user) ticket.set_custom_field_values() return ticket diff --git a/helpdesk/settings.py b/helpdesk/settings.py index 0d983e53..5acec0ff 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -14,11 +14,11 @@ import sys DEFAULT_USER_SETTINGS = { - 'login_view_ticketlist': True, - 'email_on_ticket_change': True, - 'email_on_ticket_assign': True, - 'tickets_per_page': 25, - 'use_email_as_submitter': True, + "login_view_ticketlist": True, + "email_on_ticket_change": True, + "email_on_ticket_assign": True, + "tickets_per_page": 25, + "use_email_as_submitter": True, } try: @@ -33,8 +33,8 @@ HAS_TAG_SUPPORT = False 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') +if os.environ.get("SECURE_PROXY_SSL_HEADER"): + SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True @@ -44,130 +44,168 @@ if os.environ.get('SECURE_PROXY_SSL_HEADER'): ########################################## # redirect to login page instead of the default homepage when users visits "/"? -HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(settings, - 'HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT', - False) +HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr( + settings, "HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT", False +) -HELPDESK_PUBLIC_VIEW_PROTECTOR = getattr(settings, - 'HELPDESK_PUBLIC_VIEW_PROTECTOR', - lambda _: None) +HELPDESK_PUBLIC_VIEW_PROTECTOR = getattr( + settings, "HELPDESK_PUBLIC_VIEW_PROTECTOR", lambda _: None +) -HELPDESK_STAFF_VIEW_PROTECTOR = getattr(settings, - 'HELPDESK_STAFF_VIEW_PROTECTOR', - lambda _: None) +HELPDESK_STAFF_VIEW_PROTECTOR = getattr( + settings, "HELPDESK_STAFF_VIEW_PROTECTOR", lambda _: None +) # Enable ticket and Email attachments # # Caution! Set this to False, unless you have secured access to # the uploaded files. Otherwise anyone on the Internet will be # able to download your ticket attachments. -HELPDESK_ENABLE_ATTACHMENTS = getattr(settings, - 'HELPDESK_ENABLE_ATTACHMENTS', - True) +HELPDESK_ENABLE_ATTACHMENTS = getattr(settings, "HELPDESK_ENABLE_ATTACHMENTS", True) # 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 = getattr( + settings, "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 = getattr( + settings, "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, - 'HELPDESK_ANON_ACCESS_RAISES_404', - False) +HELPDESK_ANON_ACCESS_RAISES_404 = getattr( + settings, "HELPDESK_ANON_ACCESS_RAISES_404", False +) # Disable Timeline on ticket list HELPDESK_TICKETS_TIMELINE_ENABLED = getattr( - settings, 'HELPDESK_TICKETS_TIMELINE_ENABLED', True) + 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) +HELPDESK_NAVIGATION_ENABLED = getattr(settings, "HELPDESK_NAVIGATION_ENABLED", False) # use public CDNs to serve jquery and other javascript by default? # otherwise, use built-in static copy -HELPDESK_USE_CDN = getattr(settings, 'HELPDESK_USE_CDN', False) +HELPDESK_USE_CDN = getattr(settings, "HELPDESK_USE_CDN", False) # show dropdown list of languages that ticket comments can be translated into? -HELPDESK_TRANSLATE_TICKET_COMMENTS = getattr(settings, - 'HELPDESK_TRANSLATE_TICKET_COMMENTS', - False) +HELPDESK_TRANSLATE_TICKET_COMMENTS = getattr( + settings, "HELPDESK_TRANSLATE_TICKET_COMMENTS", False +) # list of languages to offer. if set to false, # all default google translate languages will be shown. -HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG = getattr(settings, - 'HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG', - ["en", "de", "es", "fr", "it", "ru"]) +HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG = getattr( + settings, + "HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG", + ["en", "de", "es", "fr", "it", "ru"], +) # show link to 'change password' on 'User Settings' page? HELPDESK_SHOW_CHANGE_PASSWORD = getattr( - settings, 'HELPDESK_SHOW_CHANGE_PASSWORD', False) + 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) +HELPDESK_FOLLOWUP_MOD = getattr(settings, "HELPDESK_FOLLOWUP_MOD", False) # auto-subscribe user to ticket if (s)he responds to a ticket? -HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE = getattr(settings, - 'HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE', - False) +HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE = getattr( + settings, "HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE", False +) # 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', -)) +ALLOWED_URL_SCHEMES = getattr( + settings, + "ALLOWED_URL_SCHEMES", + ( + "file", + "ftp", + "ftps", + "http", + "https", + "irc", + "mailto", + "sftp", + "ssh", + "tel", + "telnet", + "tftp", + "vnc", + "xmpp", + ), +) # Ticket status choices -OPEN_STATUS = getattr(settings, 'HELPDESK_TICKET_OPEN_STATUS', 1) -REOPENED_STATUS = getattr(settings, 'HELPDESK_TICKET_REOPENED_STATUS', 2) -RESOLVED_STATUS = getattr(settings, 'HELPDESK_TICKET_RESOLVED_STATUS', 3) -CLOSED_STATUS = getattr(settings, 'HELPDESK_TICKET_CLOSED_STATUS', 4) -DUPLICATE_STATUS = getattr(settings, 'HELPDESK_TICKET_DUPLICATE_STATUS', 5) +OPEN_STATUS = getattr(settings, "HELPDESK_TICKET_OPEN_STATUS", 1) +REOPENED_STATUS = getattr(settings, "HELPDESK_TICKET_REOPENED_STATUS", 2) +RESOLVED_STATUS = getattr(settings, "HELPDESK_TICKET_RESOLVED_STATUS", 3) +CLOSED_STATUS = getattr(settings, "HELPDESK_TICKET_CLOSED_STATUS", 4) +DUPLICATE_STATUS = getattr(settings, "HELPDESK_TICKET_DUPLICATE_STATUS", 5) DEFAULT_TICKET_STATUS_CHOICES = ( - (OPEN_STATUS, _('Open')), - (REOPENED_STATUS, _('Reopened')), - (RESOLVED_STATUS, _('Resolved')), - (CLOSED_STATUS, _('Closed')), - (DUPLICATE_STATUS, _('Duplicate')), + (OPEN_STATUS, _("Open")), + (REOPENED_STATUS, _("Reopened")), + (RESOLVED_STATUS, _("Resolved")), + (CLOSED_STATUS, _("Closed")), + (DUPLICATE_STATUS, _("Duplicate")), +) +TICKET_STATUS_CHOICES = getattr( + settings, "HELPDESK_TICKET_STATUS_CHOICES", DEFAULT_TICKET_STATUS_CHOICES ) -TICKET_STATUS_CHOICES = getattr(settings, - 'HELPDESK_TICKET_STATUS_CHOICES', - DEFAULT_TICKET_STATUS_CHOICES) # List of status choices considered as "open" -DEFAULT_TICKET_OPEN_STATUSES = (OPEN_STATUS, REOPENED_STATUS) -TICKET_OPEN_STATUSES = getattr(settings, - 'HELPDESK_TICKET_OPEN_STATUSES', - DEFAULT_TICKET_OPEN_STATUSES) +DEFAULT_TICKET_OPEN_STATUSES = (OPEN_STATUS, REOPENED_STATUS) +TICKET_OPEN_STATUSES = getattr( + settings, "HELPDESK_TICKET_OPEN_STATUSES", DEFAULT_TICKET_OPEN_STATUSES +) # New status list choices depending on current ticket status DEFAULT_TICKET_STATUS_CHOICES_FLOW = { - OPEN_STATUS: (OPEN_STATUS, RESOLVED_STATUS, CLOSED_STATUS, DUPLICATE_STATUS,), - REOPENED_STATUS: (REOPENED_STATUS, RESOLVED_STATUS, CLOSED_STATUS, DUPLICATE_STATUS,), - RESOLVED_STATUS: (REOPENED_STATUS, RESOLVED_STATUS, CLOSED_STATUS,), - CLOSED_STATUS: (REOPENED_STATUS, CLOSED_STATUS,), - DUPLICATE_STATUS: (REOPENED_STATUS, DUPLICATE_STATUS,), + OPEN_STATUS: ( + OPEN_STATUS, + RESOLVED_STATUS, + CLOSED_STATUS, + DUPLICATE_STATUS, + ), + REOPENED_STATUS: ( + REOPENED_STATUS, + RESOLVED_STATUS, + CLOSED_STATUS, + DUPLICATE_STATUS, + ), + RESOLVED_STATUS: ( + REOPENED_STATUS, + RESOLVED_STATUS, + CLOSED_STATUS, + ), + CLOSED_STATUS: ( + REOPENED_STATUS, + CLOSED_STATUS, + ), + DUPLICATE_STATUS: ( + REOPENED_STATUS, + DUPLICATE_STATUS, + ), } -TICKET_STATUS_CHOICES_FLOW = getattr(settings, - 'HELPDESK_TICKET_STATUS_CHOICES_FLOW', - DEFAULT_TICKET_STATUS_CHOICES_FLOW) +TICKET_STATUS_CHOICES_FLOW = getattr( + settings, "HELPDESK_TICKET_STATUS_CHOICES_FLOW", DEFAULT_TICKET_STATUS_CHOICES_FLOW +) # Ticket priority choices DEFAULT_TICKET_PRIORITY_CHOICES = ( - (1, _('1. Critical')), - (2, _('2. High')), - (3, _('3. Normal')), - (4, _('4. Low')), - (5, _('5. Very Low')), + (1, _("1. Critical")), + (2, _("2. High")), + (3, _("3. Normal")), + (4, _("4. Low")), + (5, _("5. Very Low")), +) +TICKET_PRIORITY_CHOICES = getattr( + settings, "HELPDESK_TICKET_PRIORITY_CHOICES", DEFAULT_TICKET_PRIORITY_CHOICES ) -TICKET_PRIORITY_CHOICES = getattr(settings, - 'HELPDESK_TICKET_PRIORITY_CHOICES', - DEFAULT_TICKET_PRIORITY_CHOICES) ######################### @@ -175,59 +213,55 @@ TICKET_PRIORITY_CHOICES = getattr(settings, ######################### # Follow-ups automatic time_spent calculation -FOLLOWUP_TIME_SPENT_AUTO = getattr(settings, - 'HELPDESK_FOLLOWUP_TIME_SPENT_AUTO', - False) +FOLLOWUP_TIME_SPENT_AUTO = getattr(settings, "HELPDESK_FOLLOWUP_TIME_SPENT_AUTO", False) # Calculate time_spent according to open hours -FOLLOWUP_TIME_SPENT_OPENING_HOURS = getattr(settings, - 'HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS', - {}) +FOLLOWUP_TIME_SPENT_OPENING_HOURS = getattr( + settings, "HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS", {} +) # Holidays don't count for time_spent calculation -FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = getattr(settings, - 'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS', - ()) +FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = getattr( + settings, "HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS", () +) # Time doesn't count for listed ticket statuses -FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = getattr(settings, - 'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES', - ()) +FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = getattr( + settings, "HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES", () +) # Time doesn't count for listed queues slugs -FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = getattr(settings, - 'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES', - ()) +FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = getattr( + settings, "HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES", () +) ############################ # 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) + 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( - settings, - "HELPDESK_PUBLIC_TICKET_FORM_CLASS", - "helpdesk.forms.PublicTicketForm" + settings, "HELPDESK_PUBLIC_TICKET_FORM_CLASS", "helpdesk.forms.PublicTicketForm" ) # Custom fields constants CUSTOMFIELD_TO_FIELD_DICT = { - 'boolean': forms.BooleanField, - 'date': forms.DateField, - 'time': forms.TimeField, - 'datetime': forms.DateTimeField, - 'email': forms.EmailField, - 'url': forms.URLField, - 'ipaddress': forms.GenericIPAddressField, - 'slug': forms.SlugField, + "boolean": forms.BooleanField, + "date": forms.DateField, + "time": forms.TimeField, + "datetime": forms.DateTimeField, + "email": forms.EmailField, + "url": forms.URLField, + "ipaddress": forms.GenericIPAddressField, + "slug": forms.SlugField, } CUSTOMFIELD_DATE_FORMAT = "%Y-%m-%d" CUSTOMFIELD_TIME_FORMAT = "%H:%M:%S" @@ -238,48 +272,58 @@ CUSTOMFIELD_DATETIME_FORMAT = f"{CUSTOMFIELD_DATE_FORMAT}T%H:%M" # options for update_ticket views # ################################### -''' options for update_ticket views ''' +""" 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) -if not (HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE in (True, False) or callable(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE)): + 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.", - RuntimeWarning + RuntimeWarning, ) # show edit buttons in ticket follow ups. -HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(settings, - 'HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP', - True) +HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr( + settings, "HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP", True +) -HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST = getattr(settings, - 'HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST', - []) +HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST = getattr( + settings, "HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST", [] +) # show delete buttons in ticket follow ups if user is 'superuser' HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP = getattr( - settings, 'HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP', False) + 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) + 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) + 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) + settings, "HELPDESK_STAFF_ONLY_TICKET_CC", False +) # allow the subject to have a configurable template. HELPDESK_EMAIL_SUBJECT_TEMPLATE = getattr( - settings, 'HELPDESK_EMAIL_SUBJECT_TEMPLATE', - "{{ ticket.ticket }} {{ ticket.title|safe }} %(subject)s") + settings, + "HELPDESK_EMAIL_SUBJECT_TEMPLATE", + "{{ ticket.ticket }} {{ ticket.title|safe }} %(subject)s", +) # since django-helpdesk may not work correctly without the ticket ID # in the subject, let's do a check for it quick: if HELPDESK_EMAIL_SUBJECT_TEMPLATE.find("ticket.ticket") < 0: @@ -287,12 +331,14 @@ if HELPDESK_EMAIL_SUBJECT_TEMPLATE.find("ticket.ticket") < 0: # default fallback locale when queue locale not found HELPDESK_EMAIL_FALLBACK_LOCALE = getattr( - settings, 'HELPDESK_EMAIL_FALLBACK_LOCALE', 'en') + 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) + settings, "HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE", 512000 +) ######################################## @@ -301,7 +347,8 @@ HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr( # hide the 'assigned to' / 'Case owner' field from the 'create_ticket' view? HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr( - settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False) + settings, "HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO", False +) ################# @@ -309,33 +356,37 @@ HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr( ################# # default Queue email submission settings -QUEUE_EMAIL_BOX_TYPE = getattr(settings, 'QUEUE_EMAIL_BOX_TYPE', None) -QUEUE_EMAIL_BOX_SSL = getattr(settings, 'QUEUE_EMAIL_BOX_SSL', None) -QUEUE_EMAIL_BOX_HOST = getattr(settings, 'QUEUE_EMAIL_BOX_HOST', None) -QUEUE_EMAIL_BOX_USER = getattr(settings, 'QUEUE_EMAIL_BOX_USER', None) -QUEUE_EMAIL_BOX_PASSWORD = getattr(settings, 'QUEUE_EMAIL_BOX_PASSWORD', None) +QUEUE_EMAIL_BOX_TYPE = getattr(settings, "QUEUE_EMAIL_BOX_TYPE", None) +QUEUE_EMAIL_BOX_SSL = getattr(settings, "QUEUE_EMAIL_BOX_SSL", None) +QUEUE_EMAIL_BOX_HOST = getattr(settings, "QUEUE_EMAIL_BOX_HOST", None) +QUEUE_EMAIL_BOX_USER = getattr(settings, "QUEUE_EMAIL_BOX_USER", None) +QUEUE_EMAIL_BOX_PASSWORD = getattr(settings, "QUEUE_EMAIL_BOX_PASSWORD", None) # only process emails with a valid tracking ID? (throws away all other mail) -QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr( - settings, 'QUEUE_EMAIL_BOX_UPDATE_ONLY', False) +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) + 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', settings.SECURE_SSL_REDIRECT) + settings, "HELPDESK_USE_HTTPS_IN_EMAIL_LINK", settings.SECURE_SSL_REDIRECT +) # Default to True for backwards compatibility -HELPDESK_TEAMS_MODE_ENABLED = getattr(settings, 'HELPDESK_TEAMS_MODE_ENABLED', True) +HELPDESK_TEAMS_MODE_ENABLED = getattr(settings, "HELPDESK_TEAMS_MODE_ENABLED", True) if HELPDESK_TEAMS_MODE_ENABLED: - 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_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) + settings, "HELPDESK_KBITEM_TEAM_GETTER", lambda kbitem: kbitem.team + ) else: HELPDESK_TEAMS_MODEL = settings.AUTH_USER_MODEL HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = [] @@ -343,35 +394,38 @@ else: # show knowledgebase links? # If Teams mode is enabled then it has to be on -HELPDESK_KB_ENABLED = True if HELPDESK_TEAMS_MODE_ENABLED else getattr(settings, 'HELPDESK_KB_ENABLED', True) +HELPDESK_KB_ENABLED = ( + True + if HELPDESK_TEAMS_MODE_ENABLED + else getattr(settings, "HELPDESK_KB_ENABLED", True) +) # 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) + 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) + settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False +) ####################### # email OAUTH # ####################### HELPDESK_OAUTH = getattr( - settings, 'HELPDESK_OAUTH', { - "token_url": "", - "client_id": "", - "secret": "", - "scope": [""] - } + settings, + "HELPDESK_OAUTH", + {"token_url": "", "client_id": "", "secret": "", "scope": [""]}, ) # Set Debug Logging Level for IMAP Services. Default to '0' for No Debugging -HELPDESK_IMAP_DEBUG_LEVEL = getattr(settings, 'HELPDESK_IMAP_DEBUG_LEVEL', 0) +HELPDESK_IMAP_DEBUG_LEVEL = getattr(settings, "HELPDESK_IMAP_DEBUG_LEVEL", 0) ############################################# # file permissions - Attachment directories # @@ -379,29 +433,60 @@ HELPDESK_IMAP_DEBUG_LEVEL = getattr(settings, 'HELPDESK_IMAP_DEBUG_LEVEL', 0) # Attachment directories should be created with permission 755 (rwxr-xr-x) # Override it in your own Django settings.py -HELPDESK_ATTACHMENT_DIR_PERMS = int(getattr(settings, 'HELPDESK_ATTACHMENT_DIR_PERMS', "755"), 8) +HELPDESK_ATTACHMENT_DIR_PERMS = int( + getattr(settings, "HELPDESK_ATTACHMENT_DIR_PERMS", "755"), 8 +) -HELPDESK_VALID_EXTENSIONS = getattr(settings, 'VALID_EXTENSIONS', None) +HELPDESK_VALID_EXTENSIONS = getattr(settings, "VALID_EXTENSIONS", None) if HELPDESK_VALID_EXTENSIONS: # Print to stderr - print("VALID_EXTENSIONS is deprecated, use HELPDESK_VALID_EXTENSIONS instead", file=sys.stderr) + print( + "VALID_EXTENSIONS is deprecated, use HELPDESK_VALID_EXTENSIONS instead", + file=sys.stderr, + ) else: - HELPDESK_VALID_EXTENSIONS = getattr(settings, 'HELPDESK_VALID_EXTENSIONS', ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml']) + HELPDESK_VALID_EXTENSIONS = getattr( + settings, + "HELPDESK_VALID_EXTENSIONS", + [ + ".txt", + ".asc", + ".htm", + ".html", + ".pdf", + ".doc", + ".docx", + ".odt", + ".jpg", + ".png", + ".eml", + ], + ) + +HELPDESK_VALIDATE_ATTACHMENT_TYPES = getattr( + settings, "HELPDESK_VALIDATE_ATTACHMENT_TYPES", True +) -HELPDESK_VALIDATE_ATTACHMENT_TYPES = getattr(settings, 'HELPDESK_VALIDATE_ATTACHMENT_TYPES', True) def get_followup_webhook_urls(): - urls = os.environ.get('HELPDESK_FOLLOWUP_WEBHOOK_URLS', None) + urls = os.environ.get("HELPDESK_FOLLOWUP_WEBHOOK_URLS", None) if urls: - return re.split(r'[\s],[\s]', urls) + return re.split(r"[\s],[\s]", urls) + + +HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS = getattr( + settings, "HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS", get_followup_webhook_urls +) -HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS = getattr(settings, 'HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS', get_followup_webhook_urls) def get_new_ticket_webhook_urls(): - urls = os.environ.get('HELPDESK_NEW_TICKET_WEBHOOK_URLS', None) + urls = os.environ.get("HELPDESK_NEW_TICKET_WEBHOOK_URLS", None) if urls: - return urls.split(',') + return urls.split(",") -HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS = getattr(settings, 'HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS', get_new_ticket_webhook_urls) -HELPDESK_WEBHOOK_TIMEOUT = getattr(settings, 'HELPDESK_WEBHOOK_TIMEOUT', 3) +HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS = getattr( + settings, "HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS", get_new_ticket_webhook_urls +) + +HELPDESK_WEBHOOK_TIMEOUT = getattr(settings, "HELPDESK_WEBHOOK_TIMEOUT", 3) diff --git a/helpdesk/signals.py b/helpdesk/signals.py index 0f72ef20..d2b52dde 100644 --- a/helpdesk/signals.py +++ b/helpdesk/signals.py @@ -4,4 +4,4 @@ import django.dispatch new_ticket_done = django.dispatch.Signal() # create a signal for ticket_update view -update_ticket_done = django.dispatch.Signal() \ No newline at end of file +update_ticket_done = django.dispatch.Signal() diff --git a/helpdesk/templated_email.py b/helpdesk/templated_email.py index e68489a9..11b7a15d 100644 --- a/helpdesk/templated_email.py +++ b/helpdesk/templated_email.py @@ -1,4 +1,3 @@ - from django.conf import settings from django.utils.safestring import mark_safe import logging @@ -6,17 +5,19 @@ import os from smtplib import SMTPException -logger = logging.getLogger('helpdesk') +logger = logging.getLogger("helpdesk") -def send_templated_mail(template_name, - context, - recipients, - sender=None, - bcc=None, - fail_silently=False, - files=None, - extra_headers=None): +def send_templated_mail( + template_name, + context, + recipients, + sender=None, + bcc=None, + fail_silently=False, + files=None, + extra_headers=None, +): """ send_templated_mail() is a wrapper around Django's e-mail routines that allows us to easily send multipart (text/plain & text/html) e-mails using @@ -48,77 +49,87 @@ def send_templated_mail(template_name, """ from django.core.mail import EmailMultiAlternatives from django.template import engines - from_string = engines['django'].from_string + + from_string = engines["django"].from_string from helpdesk.models import EmailTemplate - from helpdesk.settings import HELPDESK_EMAIL_FALLBACK_LOCALE, HELPDESK_EMAIL_SUBJECT_TEMPLATE + from helpdesk.settings import ( + HELPDESK_EMAIL_FALLBACK_LOCALE, + HELPDESK_EMAIL_SUBJECT_TEMPLATE, + ) headers = extra_headers or {} - locale = context['queue'].get('locale') or HELPDESK_EMAIL_FALLBACK_LOCALE + locale = context["queue"].get("locale") or HELPDESK_EMAIL_FALLBACK_LOCALE try: t = EmailTemplate.objects.get( - template_name__iexact=template_name, locale=locale) + template_name__iexact=template_name, locale=locale + ) except EmailTemplate.DoesNotExist: try: t = EmailTemplate.objects.get( - template_name__iexact=template_name, locale__isnull=True) + 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( - HELPDESK_EMAIL_SUBJECT_TEMPLATE % { - "subject": t.subject - }).render(context).replace('\n', '').replace('\r', '') + subject_part = ( + from_string(HELPDESK_EMAIL_SUBJECT_TEMPLATE % {"subject": t.subject}) + .render(context) + .replace("\n", "") + .replace("\r", "") + ) - footer_file = os.path.join('helpdesk', locale, 'email_text_footer.txt') + footer_file = os.path.join("helpdesk", locale, "email_text_footer.txt") text_part = from_string( "%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', '
')) + if "comment" in context: + context["comment"] = mark_safe(context["comment"].replace("\r\n", "
")) html_part = from_string( "{%% extends '%s' %%}" "{%% block title %%}%s{%% endblock %%}" - "{%% block content %%}%s{%% endblock %%}" % - (email_html_base_file, t.heading, t.html) + "{%% block content %%}%s{%% endblock %%}" + % (email_html_base_file, t.heading, t.html) ).render(context) if isinstance(recipients, str): - if recipients.find(','): - recipients = recipients.split(',') + if recipients.find(","): + recipients = recipients.split(",") elif type(recipients) is not list: recipients = [recipients] - msg = EmailMultiAlternatives(subject_part, text_part, - sender or settings.DEFAULT_FROM_EMAIL, - recipients, bcc=bcc, - headers=headers) + msg = EmailMultiAlternatives( + subject_part, + text_part, + sender or settings.DEFAULT_FROM_EMAIL, + recipients, + bcc=bcc, + headers=headers, + ) msg.attach_alternative(html_part, "text/html") if files: for filename, filefield in files: - filefield.open('rb') + filefield.open("rb") content = filefield.read() msg.attach(filename, content) filefield.close() - logger.debug('Sending email to: {!r}'.format(recipients)) + logger.debug("Sending email to: {!r}".format(recipients)) try: return msg.send() except SMTPException as e: logger.exception( - 'SMTPException raised while sending email to {}'.format(recipients)) + "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 621398cc..5cd6b570 100644 --- a/helpdesk/templatetags/helpdesk_staff.py +++ b/helpdesk/templatetags/helpdesk_staff.py @@ -14,10 +14,9 @@ logger = logging.getLogger(__name__) register = Library() -@register.filter(name='is_helpdesk_staff') +@register.filter(name="is_helpdesk_staff") 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 2498e2e5..41c5661c 100644 --- a/helpdesk/templatetags/helpdesk_util.py +++ b/helpdesk/templatetags/helpdesk_util.py @@ -2,7 +2,11 @@ from datetime import datetime from django.conf import settings from django.template import Library from django.template.defaultfilters import date as date_filter -from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT +from helpdesk.forms import ( + CUSTOMFIELD_DATE_FORMAT, + CUSTOMFIELD_DATETIME_FORMAT, + CUSTOMFIELD_TIME_FORMAT, +) register = Library() @@ -10,7 +14,7 @@ register = Library() @register.filter def get(value, arg, default=None): - """ Call the dictionary get function """ + """Call the dictionary get function""" return value.get(arg, default) @@ -21,16 +25,21 @@ 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/templatetags/load_helpdesk_settings.py b/helpdesk/templatetags/load_helpdesk_settings.py index de92a551..77c0e21b 100644 --- a/helpdesk/templatetags/load_helpdesk_settings.py +++ b/helpdesk/templatetags/load_helpdesk_settings.py @@ -4,6 +4,7 @@ django-helpdesk - A Django powered ticket tracker for small enterprise. templatetags/load_helpdesk_settings.py - returns the settings as defined in django-helpdesk/helpdesk/settings.py """ + from django.template import Library from helpdesk import settings as helpdesk_settings_config @@ -13,11 +14,14 @@ def load_helpdesk_settings(request): return helpdesk_settings_config except Exception as e: import sys - print("'load_helpdesk_settings' template tag (django-helpdesk) crashed with following error:", - file=sys.stderr) + + print( + "'load_helpdesk_settings' template tag (django-helpdesk) crashed with following error:", + file=sys.stderr, + ) print(e, file=sys.stderr) - return '' + return "" register = Library() -register.filter('load_helpdesk_settings', load_helpdesk_settings) +register.filter("load_helpdesk_settings", load_helpdesk_settings) diff --git a/helpdesk/templatetags/saved_queries.py b/helpdesk/templatetags/saved_queries.py index 5956fc86..3421d1ba 100644 --- a/helpdesk/templatetags/saved_queries.py +++ b/helpdesk/templatetags/saved_queries.py @@ -5,6 +5,7 @@ templatetags/saved_queries.py - This template tag returns previously saved queries. Therefore you don't need to modify any views. """ + from django import template from django.db.models import Q from helpdesk.models import SavedSearch @@ -23,7 +24,10 @@ def saved_queries(user): return user_saved_queries except Exception as e: import sys - print("'saved_queries' template tag (django-helpdesk) crashed with following error:", - file=sys.stderr) + + print( + "'saved_queries' template tag (django-helpdesk) crashed with following error:", + file=sys.stderr, + ) print(e, file=sys.stderr) - return '' + return "" diff --git a/helpdesk/templatetags/ticket_to_link.py b/helpdesk/templatetags/ticket_to_link.py index 9111ff11..d2ee487d 100644 --- a/helpdesk/templatetags/ticket_to_link.py +++ b/helpdesk/templatetags/ticket_to_link.py @@ -19,7 +19,7 @@ import re def num_to_link(text): - if text == '': + if text == "": return text matches = [] @@ -28,7 +28,7 @@ def num_to_link(text): for match in reversed(matches): number = match.groups()[0] - url = reverse('helpdesk:view', args=[number]) + url = reverse("helpdesk:view", args=[number]) try: ticket = Ticket.objects.get(id=number) except Ticket.DoesNotExist: @@ -36,8 +36,16 @@ def num_to_link(text): if ticket: style = ticket.get_status_display() - text = "%s #%s%s" % ( - text[:match.start() + 1], url, style, match.groups()[0], text[match.end():]) + text = ( + "%s #%s%s" + % ( + text[: match.start() + 1], + url, + style, + match.groups()[0], + text[match.end() :], + ) + ) return mark_safe(text) diff --git a/helpdesk/templatetags/user_admin_url.py b/helpdesk/templatetags/user_admin_url.py index baed1241..42a8a6c8 100644 --- a/helpdesk/templatetags/user_admin_url.py +++ b/helpdesk/templatetags/user_admin_url.py @@ -20,9 +20,7 @@ def user_admin_url(action): except AttributeError: # module_name alias removed in django 1.8 model_name = user._meta.model_name.lower() - return 'admin:%s_%s_%s' % ( - user._meta.app_label, model_name, - action) + return "admin:%s_%s_%s" % (user._meta.app_label, model_name, action) register = template.Library() diff --git a/helpdesk/tests/helpers.py b/helpdesk/tests/helpers.py index e91e7d85..85088c87 100644 --- a/helpdesk/tests/helpers.py +++ b/helpdesk/tests/helpers.py @@ -8,16 +8,15 @@ import sys User = get_user_model() -def get_user(username='helpdesk.staff', - password='password', - is_staff=False, - is_superuser=False): +def get_user( + username="helpdesk.staff", password="password", is_staff=False, is_superuser=False +): try: user = User.objects.get(username=username) except User.DoesNotExist: - user = User.objects.create_user(username=username, - password=password, - email='%s@example.com' % username) + user = User.objects.create_user( + username=username, password=password, email="%s@example.com" % username + ) user.is_staff = is_staff user.is_superuser = is_superuser user.save() @@ -32,7 +31,6 @@ def get_staff_user(): def reload_urlconf(urlconf=None): - from importlib import reload if urlconf is None: @@ -47,25 +45,29 @@ def reload_urlconf(urlconf=None): reload(sys.modules[urlconf]) from django.urls import clear_url_caches + clear_url_caches() def create_ticket(**kwargs): - q = kwargs.get('queue', None) + q = kwargs.get("queue", None) if q is None: try: q = Queue.objects.all()[0] except IndexError: - q = Queue.objects.create(title='Test Q', slug='test', ) + q = Queue.objects.create( + title="Test Q", + slug="test", + ) data = { - 'title': "I wish to register a complaint", - 'queue': q, + "title": "I wish to register a complaint", + "queue": q, } data.update(kwargs) return Ticket.objects.create(**data) -HELPDESK_URLCONF = 'helpdesk.urls' +HELPDESK_URLCONF = "helpdesk.urls" def print_response(response, stdout=False): diff --git a/helpdesk/tests/test_api.py b/helpdesk/tests/test_api.py index b2c93aa8..518ae744 100644 --- a/helpdesk/tests/test_api.py +++ b/helpdesk/tests/test_api.py @@ -1,4 +1,3 @@ - import base64 from collections import OrderedDict from datetime import datetime @@ -13,7 +12,7 @@ from rest_framework.status import ( HTTP_201_CREATED, HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST, - HTTP_403_FORBIDDEN + HTTP_403_FORBIDDEN, ) from rest_framework.test import APITestCase @@ -24,106 +23,124 @@ class TicketTest(APITestCase): @classmethod def setUpTestData(cls): cls.queue = Queue.objects.create( - title='Test Queue', - slug='test-queue', + title="Test Queue", + slug="test-queue", ) def test_create_api_ticket_not_authenticated_user(self): - response = self.client.post('/api/tickets/') + response = self.client.post("/api/tickets/") self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) def test_create_api_ticket_authenticated_non_staff_user(self): - non_staff_user = User.objects.create_user(username='test') + non_staff_user = User.objects.create_user(username="test") self.client.force_authenticate(non_staff_user) - response = self.client.post('/api/tickets/') + response = self.client.post("/api/tickets/") self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) def test_create_api_ticket_no_data(self): - staff_user = User.objects.create_user(username='test', is_staff=True) + staff_user = User.objects.create_user(username="test", is_staff=True) self.client.force_authenticate(staff_user) - response = self.client.post('/api/tickets/') + response = self.client.post("/api/tickets/") self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, { - 'queue': [ErrorDetail(string='This field is required.', code='required')], - 'title': [ErrorDetail(string='This field is required.', code='required')] - }) + self.assertEqual( + response.data, + { + "queue": [ + ErrorDetail(string="This field is required.", code="required") + ], + "title": [ + ErrorDetail(string="This field is required.", code="required") + ], + }, + ) self.assertFalse(Ticket.objects.exists()) def test_create_api_ticket_wrong_date_format(self): - staff_user = User.objects.create_user(username='test', is_staff=True) + staff_user = User.objects.create_user(username="test", is_staff=True) self.client.force_authenticate(staff_user) - response = self.client.post('/api/tickets/', { - 'queue': self.queue.id, - 'title': 'Test title', - 'due_date': 'monday, 1st of may 2022' - }) + response = self.client.post( + "/api/tickets/", + { + "queue": self.queue.id, + "title": "Test title", + "due_date": "monday, 1st of may 2022", + }, + ) self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, { - 'due_date': [ErrorDetail(string='Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].', code='invalid')] - }) + self.assertEqual( + response.data, + { + "due_date": [ + ErrorDetail( + string="Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].", + code="invalid", + ) + ] + }, + ) self.assertFalse(Ticket.objects.exists()) def test_create_api_ticket_authenticated_staff_user(self): - staff_user = User.objects.create_user(username='test', is_staff=True) + staff_user = User.objects.create_user(username="test", is_staff=True) self.client.force_authenticate(staff_user) - response = self.client.post('/api/tickets/', { - 'queue': self.queue.id, - 'title': 'Test title', - 'description': 'Test description\nMulti lines', - 'submitter_email': 'test@mail.com', - 'priority': 4 - }) + response = self.client.post( + "/api/tickets/", + { + "queue": self.queue.id, + "title": "Test title", + "description": "Test description\nMulti lines", + "submitter_email": "test@mail.com", + "priority": 4, + }, + ) 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.submitter_email, 'test@mail.com') + self.assertEqual(created_ticket.title, "Test title") + 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) def test_create_api_ticket_with_basic_auth(self): - username = 'admin' - password = 'admin' - User.objects.create_user( - username=username, password=password, is_staff=True) + username = "admin" + password = "admin" + 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') + test_user = User.objects.create_user(username="test") + 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/', + "/api/tickets/", { - 'queue': self.queue.id, - 'title': 'Title', - 'description': 'Description', - 'resolution': 'Resolution', - 'assigned_to': test_user.id, - 'submitter_email': 'test@mail.com', - 'status': Ticket.RESOLVED_STATUS, - 'priority': 1, - 'on_hold': True, - 'due_date': self.due_date, - 'merged_to': merge_ticket.id - } + "queue": self.queue.id, + "title": "Title", + "description": "Description", + "resolution": "Resolution", + "assigned_to": test_user.id, + "submitter_email": "test@mail.com", + "status": Ticket.RESOLVED_STATUS, + "priority": 1, + "on_hold": True, + "due_date": self.due_date, + "merged_to": merge_ticket.id, + }, ) self.assertEqual(response.status_code, HTTP_201_CREATED) created_ticket = Ticket.objects.last() - self.assertEqual(created_ticket.title, 'Title') - self.assertEqual(created_ticket.description, 'Description') + self.assertEqual(created_ticket.title, "Title") + self.assertEqual(created_ticket.description, "Description") # 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.submitter_email, "test@mail.com") self.assertEqual(created_ticket.priority, 1) # on_hold is False on creation self.assertFalse(created_ticket.on_hold) @@ -134,39 +151,37 @@ class TicketTest(APITestCase): 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') + staff_user = User.objects.create_user(username="admin", is_staff=True) + 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') + test_user = User.objects.create_user(username="test") + merge_ticket = Ticket.objects.create(queue=self.queue, title="merge ticket") self.client.force_authenticate(staff_user) response = self.client.put( - '/api/tickets/%d/' % test_ticket.id, + "/api/tickets/%d/" % test_ticket.id, { - 'queue': self.queue.id, - 'title': 'Title', - 'description': 'Description', - 'resolution': 'Resolution', - 'assigned_to': test_user.id, - 'submitter_email': 'test@mail.com', - 'status': Ticket.RESOLVED_STATUS, - 'priority': 1, - 'on_hold': True, - 'due_date': self.due_date, - 'merged_to': merge_ticket.id - } + "queue": self.queue.id, + "title": "Title", + "description": "Description", + "resolution": "Resolution", + "assigned_to": test_user.id, + "submitter_email": "test@mail.com", + "status": Ticket.RESOLVED_STATUS, + "priority": 1, + "on_hold": True, + "due_date": self.due_date, + "merged_to": merge_ticket.id, + }, ) self.assertEqual(response.status_code, HTTP_200_OK) test_ticket.refresh_from_db() - self.assertEqual(test_ticket.title, 'Title') - self.assertEqual(test_ticket.description, 'Description') - self.assertEqual(test_ticket.resolution, 'Resolution') + self.assertEqual(test_ticket.title, "Title") + self.assertEqual(test_ticket.description, "Description") + self.assertEqual(test_ticket.resolution, "Resolution") self.assertEqual(test_ticket.assigned_to, test_user) - self.assertEqual(test_ticket.submitter_email, 'test@mail.com') + self.assertEqual(test_ticket.submitter_email, "test@mail.com") self.assertEqual(test_ticket.priority, 1) self.assertTrue(test_ticket.on_hold) self.assertEqual(test_ticket.status, Ticket.RESOLVED_STATUS) @@ -174,236 +189,264 @@ class TicketTest(APITestCase): self.assertEqual(test_ticket.merged_to, merge_ticket) 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') + staff_user = User.objects.create_user(username="admin", is_staff=True) + test_ticket = Ticket.objects.create(queue=self.queue, title="Test ticket") self.client.force_authenticate(staff_user) response = self.client.patch( - '/api/tickets/%d/' % test_ticket.id, + "/api/tickets/%d/" % test_ticket.id, { - 'description': 'New description', - } + "description": "New description", + }, ) self.assertEqual(response.status_code, HTTP_200_OK) test_ticket.refresh_from_db() - self.assertEqual(test_ticket.description, 'New description') + self.assertEqual(test_ticket.description, "New description") 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') + staff_user = User.objects.create_user(username="admin", is_staff=True) + 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) + response = self.client.delete("/api/tickets/%d/" % test_ticket.id) self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) self.assertFalse(Ticket.objects.exists()) - @freeze_time('2022-06-30 23:09:44') + @freeze_time("2022-06-30 23:09:44") def test_create_api_ticket_with_custom_fields(self): # Create custom fields for field_type, field_display in CustomField.DATA_TYPE_CHOICES: extra_data = {} - if field_type in ('varchar', 'text'): - extra_data['max_length'] = 10 - if field_type == 'integer': + if field_type in ("varchar", "text"): + extra_data["max_length"] = 10 + if field_type == "integer": # Set one field as required to test error if not provided - extra_data['required'] = True - if field_type == 'decimal': - extra_data['max_length'] = 7 - extra_data['decimal_places'] = 3 - if field_type == 'list': - extra_data['list_values'] = '''Green + extra_data["required"] = True + if field_type == "decimal": + extra_data["max_length"] = 7 + extra_data["decimal_places"] = 3 + if field_type == "list": + extra_data["list_values"] = """Green Blue Red - Yellow''' + Yellow""" CustomField.objects.create( - name=field_type, label=field_display, data_type=field_type, **extra_data) + name=field_type, label=field_display, data_type=field_type, **extra_data + ) - staff_user = User.objects.create_user(username='test', is_staff=True) + staff_user = User.objects.create_user(username="test", is_staff=True) self.client.force_authenticate(staff_user) # Test creation without providing required field - response = self.client.post('/api/tickets/', { - 'queue': self.queue.id, - 'title': 'Test title', - 'description': 'Test description\nMulti lines', - 'submitter_email': 'test@mail.com', - 'priority': 4 - }) + response = self.client.post( + "/api/tickets/", + { + "queue": self.queue.id, + "title": "Test title", + "description": "Test description\nMulti lines", + "submitter_email": "test@mail.com", + "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/', { - 'queue': self.queue.id, - 'title': 'Test title', - 'description': 'Test description\nMulti lines', - 'submitter_email': 'test@mail.com', - 'priority': 4, - 'custom_varchar': 'test', - 'custom_text': 'multi\nline', - 'custom_integer': '1', - 'custom_decimal': '42.987', - 'custom_list': 'Red', - 'custom_boolean': True, - 'custom_date': '2022-4-11', - 'custom_time': '23:59:59', - 'custom_datetime': '2022-4-10 18:27', - 'custom_email': 'email@test.com', - 'custom_url': 'http://django-helpdesk.readthedocs.org/', - 'custom_ipaddress': '127.0.0.1', - 'custom_slug': 'test-slug', - }) + response = self.client.post( + "/api/tickets/", + { + "queue": self.queue.id, + "title": "Test title", + "description": "Test description\nMulti lines", + "submitter_email": "test@mail.com", + "priority": 4, + "custom_varchar": "test", + "custom_text": "multi\nline", + "custom_integer": "1", + "custom_decimal": "42.987", + "custom_list": "Red", + "custom_boolean": True, + "custom_date": "2022-4-11", + "custom_time": "23:59:59", + "custom_datetime": "2022-4-10 18:27", + "custom_email": "email@test.com", + "custom_url": "http://django-helpdesk.readthedocs.org/", + "custom_ipaddress": "127.0.0.1", + "custom_slug": "test-slug", + }, + ) self.assertEqual(response.status_code, HTTP_201_CREATED) # Check all fields with data returned from the response - self.assertEqual(response.data, { - 'id': 1, - 'queue': 1, - 'title': 'Test title', - 'description': 'Test description\nMulti lines', - 'resolution': None, - 'submitter_email': 'test@mail.com', - 'assigned_to': None, - 'status': 1, - 'on_hold': False, - 'priority': 4, - 'due_date': None, - 'merged_to': None, - 'followup_set': [OrderedDict([ - ('id', 1), - ('ticket', 1), - ('user', 1), - ('title', 'Ticket Opened'), - ('comment', 'Test description\nMulti lines'), - ('public', True), - ('new_status', None), - ('time_spent', None), - ('followupattachment_set', []), - ('date', '2022-06-30T23:09:44'), - ('message_id', None), - ])], - 'custom_varchar': 'test', - 'custom_text': 'multi\nline', - 'custom_integer': 1, - 'custom_decimal': '42.987', - 'custom_list': 'Red', - 'custom_boolean': True, - 'custom_date': '2022-04-11', - 'custom_time': '23:59:59', - 'custom_datetime': '2022-04-10T18:27', - 'custom_email': 'email@test.com', - 'custom_url': 'http://django-helpdesk.readthedocs.org/', - 'custom_ipaddress': '127.0.0.1', - 'custom_slug': 'test-slug' - }) + self.assertEqual( + response.data, + { + "id": 1, + "queue": 1, + "title": "Test title", + "description": "Test description\nMulti lines", + "resolution": None, + "submitter_email": "test@mail.com", + "assigned_to": None, + "status": 1, + "on_hold": False, + "priority": 4, + "due_date": None, + "merged_to": None, + "followup_set": [ + OrderedDict( + [ + ("id", 1), + ("ticket", 1), + ("user", 1), + ("title", "Ticket Opened"), + ("comment", "Test description\nMulti lines"), + ("public", True), + ("new_status", None), + ("time_spent", None), + ("followupattachment_set", []), + ("date", "2022-06-30T23:09:44"), + ("message_id", None), + ] + ) + ], + "custom_varchar": "test", + "custom_text": "multi\nline", + "custom_integer": 1, + "custom_decimal": "42.987", + "custom_list": "Red", + "custom_boolean": True, + "custom_date": "2022-04-11", + "custom_time": "23:59:59", + "custom_datetime": "2022-04-10T18:27", + "custom_email": "email@test.com", + "custom_url": "http://django-helpdesk.readthedocs.org/", + "custom_ipaddress": "127.0.0.1", + "custom_slug": "test-slug", + }, + ) def test_create_api_ticket_with_attachment(self): - staff_user = User.objects.create_user(username='test', is_staff=True) + 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') - response = self.client.post('/api/tickets/', { - 'queue': self.queue.id, - 'title': 'Test title', - 'description': 'Test description\nMulti lines', - 'submitter_email': 'test@mail.com', - 'priority': 4, - 'attachment': test_file - }) + "file.jpg", b"file_content", content_type="image/jpg" + ) + response = self.client.post( + "/api/tickets/", + { + "queue": self.queue.id, + "title": "Test title", + "description": "Test description\nMulti lines", + "submitter_email": "test@mail.com", + "priority": 4, + "attachment": test_file, + }, + ) 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.submitter_email, 'test@mail.com') + self.assertEqual(created_ticket.title, "Test title") + 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, - f'helpdesk/attachments/test-queue-1-{created_ticket.secret_key}/1/file.jpg' + f"helpdesk/attachments/test-queue-1-{created_ticket.secret_key}/1/file.jpg", ) def test_create_follow_up_with_attachments(self): - staff_user = User.objects.create_user(username='test', is_staff=True) + 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') + ticket = Ticket.objects.create(queue=self.queue, title="Test") test_file_1 = SimpleUploadedFile( - 'file.jpg', b'file_content', content_type='image/jpg') + "file.jpg", b"file_content", content_type="image/jpg" + ) test_file_2 = SimpleUploadedFile( - 'doc.pdf', b'Doc content', content_type='application/pdf') + "doc.pdf", b"Doc content", content_type="application/pdf" + ) - response = self.client.post('/api/followups/', { - 'ticket': ticket.id, - 'title': 'Test', - 'comment': 'Test answer\nMulti lines', - 'attachments': [ - test_file_1, - test_file_2 - ] - }) + response = self.client.post( + "/api/followups/", + { + "ticket": ticket.id, + "title": "Test", + "comment": "Test answer\nMulti lines", + "attachments": [test_file_1, test_file_2], + }, + ) self.assertEqual(response.status_code, HTTP_201_CREATED) created_followup = ticket.followup_set.last() - self.assertEqual(created_followup.title, 'Test') - self.assertEqual(created_followup.comment, 'Test answer\nMulti lines') + 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') + created_followup.followupattachment_set.first().filename, "doc.pdf" + ) self.assertEqual( - created_followup.followupattachment_set.first().mime_type, 'application/pdf') + created_followup.followupattachment_set.first().mime_type, "application/pdf" + ) self.assertEqual( - created_followup.followupattachment_set.last().filename, 'file.jpg') + created_followup.followupattachment_set.last().filename, "file.jpg" + ) self.assertEqual( - created_followup.followupattachment_set.last().mime_type, 'image/jpg') + created_followup.followupattachment_set.last().mime_type, "image/jpg" + ) class UserTicketTest(APITestCase): def setUp(self): - self.queue = Queue.objects.create(title='Test queue') - self.user = User.objects.create_user(username='test') + self.queue = Queue.objects.create(title="Test queue") + self.user = User.objects.create_user(username="test") self.client.force_authenticate(self.user) def test_get_user_tickets(self): - user = User.objects.create_user(username='test2', email="foo@example.com") + user = User.objects.create_user(username="test2", email="foo@example.com") ticket_1 = Ticket.objects.create( - queue=self.queue, title='Test 1', - submitter_email="foo@example.com") + queue=self.queue, title="Test 1", submitter_email="foo@example.com" + ) ticket_2 = Ticket.objects.create( - queue=self.queue, title='Test 2', - submitter_email="bar@example.com") + queue=self.queue, title="Test 2", submitter_email="bar@example.com" + ) ticket_3 = Ticket.objects.create( - queue=self.queue, title='Test 3', - submitter_email="foo@example.com") + queue=self.queue, title="Test 3", submitter_email="foo@example.com" + ) self.client.force_authenticate(user) - response = self.client.get('/api/user_tickets/') + response = self.client.get("/api/user_tickets/") self.assertEqual(response.status_code, HTTP_200_OK) self.assertEqual(len(response.data["results"]), 2) - self.assertEqual(response.data["results"][0]['id'], ticket_3.id) - self.assertEqual(response.data["results"][1]['id'], ticket_1.id) + self.assertEqual(response.data["results"][0]["id"], ticket_3.id) + self.assertEqual(response.data["results"][1]["id"], ticket_1.id) def test_staff_user(self): - staff_user = User.objects.create_user(username='test2', is_staff=True, email="staff@example.com") + staff_user = User.objects.create_user( + username="test2", is_staff=True, email="staff@example.com" + ) ticket_1 = Ticket.objects.create( - queue=self.queue, title='Test 1', - submitter_email="staff@example.com") + queue=self.queue, title="Test 1", submitter_email="staff@example.com" + ) ticket_2 = Ticket.objects.create( - queue=self.queue, title='Test 2', - submitter_email="foo@example.com") + queue=self.queue, title="Test 2", submitter_email="foo@example.com" + ) self.client.force_authenticate(staff_user) - response = self.client.get('/api/user_tickets/') + response = self.client.get("/api/user_tickets/") self.assertEqual(response.status_code, HTTP_200_OK) self.assertEqual(len(response.data["results"]), 1) def test_not_logged_in_user(self): ticket_1 = Ticket.objects.create( - queue=self.queue, title='Test 1', - submitter_email="ex@example.com") + queue=self.queue, title="Test 1", submitter_email="ex@example.com" + ) self.client.logout() - response = self.client.get('/api/user_tickets/') + response = self.client.get("/api/user_tickets/") self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - - diff --git a/helpdesk/tests/test_attachments.py b/helpdesk/tests/test_attachments.py index 9476facf..e53ebb5e 100644 --- a/helpdesk/tests/test_attachments.py +++ b/helpdesk/tests/test_attachments.py @@ -12,162 +12,161 @@ from unittest import mock from unittest.case import skip -MEDIA_DIR = os.path.join(gettempdir(), 'helpdesk_test_media') +MEDIA_DIR = os.path.join(gettempdir(), "helpdesk_test_media") @override_settings(MEDIA_ROOT=MEDIA_DIR) class AttachmentIntegrationTests(TestCase): - - fixtures = ['emailtemplate.json'] + fixtures = ["emailtemplate.json"] def setUp(self): self.queue_public = models.Queue.objects.create( - title='Public Queue', - slug='pub_q', + title="Public Queue", + slug="pub_q", allow_public_submission=True, - new_ticket_cc='new.public@example.com', - updated_ticket_cc='update.public@example.com', + new_ticket_cc="new.public@example.com", + updated_ticket_cc="update.public@example.com", ) self.queue_private = models.Queue.objects.create( - title='Private Queue', - slug='priv_q', + title="Private Queue", + slug="priv_q", allow_public_submission=False, - new_ticket_cc='new.private@example.com', - updated_ticket_cc='update.private@example.com', + new_ticket_cc="new.private@example.com", + updated_ticket_cc="update.private@example.com", ) self.ticket_data = { - 'title': 'Test Ticket Title', - 'body': 'Test Ticket Desc', - 'priority': 3, - 'submitter_email': 'submitter@example.com', + "title": "Test Ticket Title", + "body": "Test Ticket Desc", + "priority": 3, + "submitter_email": "submitter@example.com", } def test_create_pub_ticket_with_attachment(self): test_file = SimpleUploadedFile( - 'test_att.txt', b'attached file content', 'text/plain') + "test_att.txt", b"attached file content", "text/plain" + ) post_data = self.ticket_data.copy() - post_data.update({ - 'queue': self.queue_public.id, - 'attachment': test_file, - }) + post_data.update( + { + "queue": self.queue_public.id, + "attachment": test_file, + } + ) # 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']) + 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') + 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, - 'attachment': test_file, - }) + post_data.update( + { + "queue": self.queue_public.id, + "attachment": test_file, + } + ) # 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']) - with open(os.path.join(MEDIA_DIR, att.file.name), encoding="utf-8") as file_on_disk: - disk_content = smart_str(file_on_disk.read(), 'utf-8') - self.assertEqual(disk_content, 'โจ') + followup__ticket=response.context["ticket"] + ) + with open( + os.path.join(MEDIA_DIR, att.file.name), encoding="utf-8" + ) as file_on_disk: + disk_content = smart_str(file_on_disk.read(), "utf-8") + self.assertEqual(disk_content, "โจ") -@mock.patch.object(models.FollowUp, 'save', autospec=True) -@mock.patch.object(models.FollowUpAttachment, 'save', autospec=True) -@mock.patch.object(models.Ticket, 'save', autospec=True) -@mock.patch.object(models.Queue, 'save', autospec=True) +@mock.patch.object(models.FollowUp, "save", autospec=True) +@mock.patch.object(models.FollowUpAttachment, "save", autospec=True) +@mock.patch.object(models.Ticket, "save", autospec=True) +@mock.patch.object(models.Queue, "save", autospec=True) class AttachmentUnitTests(TestCase): - def setUp(self): self.file_attrs = { - 'filename': '°ßäöü.txt', - 'content': 'โจ'.encode('utf-8'), - 'content-type': 'text/utf8', + "filename": "°ßäöü.txt", + "content": "โจ".encode("utf-8"), + "content-type": "text/utf8", } self.test_file = SimpleUploadedFile.from_dict(self.file_attrs) self.follow_up = models.FollowUp.objects.create( - ticket=models.Ticket.objects.create( - queue=models.Queue.objects.create() - ) + ticket=models.Ticket.objects.create(queue=models.Queue.objects.create()) ) @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] + 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] mock_att_save.assert_called_with( file=self.test_file, - filename=self.file_attrs['filename'], - mime_type=self.file_attrs['content-type'], - size=len(self.file_attrs['content']), - followup=self.follow_up - ) - self.assertEqual(filename, self.file_attrs['filename']) - - def test_autofill(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save): - """ check utf-8 data is parsed correctly """ - obj = models.FollowUpAttachment.objects.create( + filename=self.file_attrs["filename"], + mime_type=self.file_attrs["content-type"], + size=len(self.file_attrs["content"]), followup=self.follow_up, - file=self.test_file + ) + self.assertEqual(filename, self.file_attrs["filename"]) + + def test_autofill( + self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save + ): + """check utf-8 data is parsed correctly""" + obj = models.FollowUpAttachment.objects.create( + followup=self.follow_up, file=self.test_file ) obj.save() - self.assertEqual(obj.file.name, self.file_attrs['filename']) - self.assertEqual(obj.file.size, len(self.file_attrs['content'])) + self.assertEqual(obj.file.name, self.file_attrs["filename"]) + self.assertEqual(obj.file.size, len(self.file_attrs["content"])) self.assertEqual(obj.file.file.content_type, "text/utf8") - def test_kbi_attachment(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save): - """ check utf-8 data is parsed correctly """ + def test_kbi_attachment( + self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save + ): + """check utf-8 data is parsed correctly""" kbcategory = models.KBCategory.objects.create( - title="Title", - slug="slug", - description="Description" + title="Title", slug="slug", description="Description" ) kbitem = models.KBItem.objects.create( - category=kbcategory, - title="Title", - question="Question", - answer="Answer" + category=kbcategory, title="Title", question="Question", answer="Answer" ) - obj = models.KBIAttachment.objects.create( - kbitem=kbitem, - file=self.test_file - ) + obj = models.KBIAttachment.objects.create(kbitem=kbitem, file=self.test_file) obj.save() - self.assertEqual(obj.filename, self.file_attrs['filename']) - self.assertEqual(obj.file.size, len(self.file_attrs['content'])) + self.assertEqual(obj.filename, self.file_attrs["filename"]) + self.assertEqual(obj.file.size, len(self.file_attrs["content"])) self.assertEqual(obj.mime_type, "text/plain") @skip("model in lib not patched") @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] + 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 attachment_obj = mock_att_save.return_value mock_att_save.assert_called_once_with(attachment_obj) self.assertIsInstance(attachment_obj, models.FollowUpAttachment) - self.assertEqual(attachment_obj.filename, self.file_attrs['filename']) + self.assertEqual(attachment_obj.filename, self.file_attrs["filename"]) def tearDownModule(): diff --git a/helpdesk/tests/test_checklist.py b/helpdesk/tests/test_checklist.py index 190b2173..3454f47a 100644 --- a/helpdesk/tests/test_checklist.py +++ b/helpdesk/tests/test_checklist.py @@ -8,245 +8,254 @@ from helpdesk.models import Checklist, ChecklistTask, ChecklistTemplate, Queue, class TicketChecklistTestCase(TestCase): @classmethod def setUpTestData(cls): - user = get_user_model().objects.create_user('User', password='pass') + user = get_user_model().objects.create_user("User", password="pass") user.is_staff = True user.save() cls.user = user def setUp(self) -> None: - self.client.login(username='User', password='pass') + self.client.login(username="User", password="pass") - self.ticket = Ticket.objects.create(queue=Queue.objects.create(title='Queue', slug='queue')) + self.ticket = Ticket.objects.create( + queue=Queue.objects.create(title="Queue", slug="queue") + ) def test_create_checklist(self): self.assertEqual(self.ticket.checklists.count(), 0) - checklist_name = 'test empty checklist' + checklist_name = "test empty checklist" response = self.client.post( - reverse('helpdesk:view', kwargs={'ticket_id': self.ticket.id}), - data={'name': checklist_name}, - follow=True + reverse("helpdesk:view", kwargs={"ticket_id": self.ticket.id}), + data={"name": checklist_name}, + follow=True, ) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'helpdesk/checklist_form.html') + self.assertTemplateUsed(response, "helpdesk/checklist_form.html") self.assertContains(response, checklist_name) self.assertEqual(self.ticket.checklists.count(), 1) def test_create_checklist_from_template(self): self.assertEqual(self.ticket.checklists.count(), 0) - checklist_name = 'test checklist from template' + checklist_name = "test checklist from template" checklist_template = ChecklistTemplate.objects.create( - name='Test template', - task_list=['first', 'second', 'last'] + name="Test template", task_list=["first", "second", "last"] ) response = self.client.post( - reverse('helpdesk:view', kwargs={'ticket_id': self.ticket.id}), - data={'name': checklist_name, 'checklist_template': checklist_template.id}, - follow=True + reverse("helpdesk:view", kwargs={"ticket_id": self.ticket.id}), + data={"name": checklist_name, "checklist_template": checklist_template.id}, + follow=True, ) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'helpdesk/checklist_form.html') + self.assertTemplateUsed(response, "helpdesk/checklist_form.html") self.assertContains(response, checklist_name) self.assertEqual(self.ticket.checklists.count(), 1) created_checklist = self.ticket.checklists.get() self.assertEqual(created_checklist.tasks.count(), 3) - self.assertEqual(created_checklist.tasks.all()[0].description, 'first') - self.assertEqual(created_checklist.tasks.all()[1].description, 'second') - self.assertEqual(created_checklist.tasks.all()[2].description, 'last') + self.assertEqual(created_checklist.tasks.all()[0].description, "first") + self.assertEqual(created_checklist.tasks.all()[1].description, "second") + self.assertEqual(created_checklist.tasks.all()[2].description, "last") def test_edit_checklist(self): - checklist = self.ticket.checklists.create(name='Test checklist') - first_task = checklist.tasks.create(description='First task', position=1) - checklist.tasks.create(description='To delete task', position=2) + checklist = self.ticket.checklists.create(name="Test checklist") + first_task = checklist.tasks.create(description="First task", position=1) + checklist.tasks.create(description="To delete task", position=2) - url = reverse('helpdesk:edit_ticket_checklist', kwargs={ - 'ticket_id': self.ticket.id, - 'checklist_id': checklist.id, - }) + url = reverse( + "helpdesk:edit_ticket_checklist", + kwargs={ + "ticket_id": self.ticket.id, + "checklist_id": checklist.id, + }, + ) response = self.client.get(url) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'helpdesk/checklist_form.html') - self.assertContains(response, 'Test checklist') - self.assertContains(response, 'First task') - self.assertContains(response, 'To delete task') + self.assertTemplateUsed(response, "helpdesk/checklist_form.html") + self.assertContains(response, "Test checklist") + self.assertContains(response, "First task") + self.assertContains(response, "To delete task") response = self.client.post( url, data={ - 'name': 'New name', - 'tasks-TOTAL_FORMS': 3, - 'tasks-INITIAL_FORMS': 2, - 'tasks-0-id': '1', - 'tasks-0-description': 'First task edited', - 'tasks-0-position': '2', - 'tasks-1-id': '2', - 'tasks-1-description': 'To delete task', - 'tasks-1-DELETE': 'on', - 'tasks-1-position': '2', - 'tasks-2-description': 'New first task', - 'tasks-2-position': '1', + "name": "New name", + "tasks-TOTAL_FORMS": 3, + "tasks-INITIAL_FORMS": 2, + "tasks-0-id": "1", + "tasks-0-description": "First task edited", + "tasks-0-position": "2", + "tasks-1-id": "2", + "tasks-1-description": "To delete task", + "tasks-1-DELETE": "on", + "tasks-1-position": "2", + "tasks-2-description": "New first task", + "tasks-2-position": "1", }, - follow=True + follow=True, ) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'helpdesk/ticket.html') + self.assertTemplateUsed(response, "helpdesk/ticket.html") checklist.refresh_from_db() - self.assertEqual(checklist.name, 'New name') + self.assertEqual(checklist.name, "New name") self.assertEqual(checklist.tasks.count(), 2) first_task.refresh_from_db() - self.assertEqual(first_task.description, 'First task edited') - self.assertEqual(checklist.tasks.all()[0].description, 'New first task') - self.assertEqual(checklist.tasks.all()[1].description, 'First task edited') + self.assertEqual(first_task.description, "First task edited") + self.assertEqual(checklist.tasks.all()[0].description, "New first task") + self.assertEqual(checklist.tasks.all()[1].description, "First task edited") def test_delete_checklist(self): - checklist = self.ticket.checklists.create(name='Test checklist') - checklist.tasks.create(description='First task', position=1) + checklist = self.ticket.checklists.create(name="Test checklist") + checklist.tasks.create(description="First task", position=1) self.assertEqual(Checklist.objects.count(), 1) self.assertEqual(ChecklistTask.objects.count(), 1) response = self.client.post( reverse( - 'helpdesk:delete_ticket_checklist', - kwargs={'ticket_id': self.ticket.id, 'checklist_id': checklist.id} + "helpdesk:delete_ticket_checklist", + kwargs={"ticket_id": self.ticket.id, "checklist_id": checklist.id}, ), - follow=True + follow=True, ) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'helpdesk/ticket.html') + self.assertTemplateUsed(response, "helpdesk/ticket.html") self.assertEqual(Checklist.objects.count(), 0) self.assertEqual(ChecklistTask.objects.count(), 0) def test_mark_task_as_done(self): - checklist = self.ticket.checklists.create(name='Test checklist') - task = checklist.tasks.create(description='Task', position=1) + checklist = self.ticket.checklists.create(name="Test checklist") + task = checklist.tasks.create(description="Task", position=1) self.assertIsNone(task.completion_date) self.assertEqual(self.ticket.followup_set.count(), 0) response = self.client.post( - reverse('helpdesk:update', kwargs={'ticket_id': self.ticket.id}), - data={ - f'checklist-{checklist.id}': task.id - }, - follow=True + reverse("helpdesk:update", kwargs={"ticket_id": self.ticket.id}), + data={f"checklist-{checklist.id}": task.id}, + follow=True, ) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'helpdesk/ticket.html') + self.assertTemplateUsed(response, "helpdesk/ticket.html") self.assertEqual(self.ticket.followup_set.count(), 1) followup = self.ticket.followup_set.get() self.assertEqual(followup.ticketchange_set.count(), 1) - self.assertEqual(followup.ticketchange_set.get().old_value, 'To do') - self.assertEqual(followup.ticketchange_set.get().new_value, 'Completed') + self.assertEqual(followup.ticketchange_set.get().old_value, "To do") + self.assertEqual(followup.ticketchange_set.get().new_value, "Completed") task.refresh_from_db() self.assertIsNotNone(task.completion_date) def test_mark_task_as_undone(self): - checklist = self.ticket.checklists.create(name='Test checklist') - task = checklist.tasks.create(description='Task', position=1, completion_date=datetime(2023, 5, 1)) + checklist = self.ticket.checklists.create(name="Test checklist") + task = checklist.tasks.create( + description="Task", position=1, completion_date=datetime(2023, 5, 1) + ) self.assertIsNotNone(task.completion_date) self.assertEqual(self.ticket.followup_set.count(), 0) response = self.client.post( - reverse('helpdesk:update', kwargs={'ticket_id': self.ticket.id}), - follow=True + reverse("helpdesk:update", kwargs={"ticket_id": self.ticket.id}), + follow=True, ) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'helpdesk/ticket.html') + self.assertTemplateUsed(response, "helpdesk/ticket.html") self.assertEqual(self.ticket.followup_set.count(), 1) followup = self.ticket.followup_set.get() self.assertEqual(followup.ticketchange_set.count(), 1) - self.assertEqual(followup.ticketchange_set.get().old_value, 'Completed') - self.assertEqual(followup.ticketchange_set.get().new_value, 'To do') + self.assertEqual(followup.ticketchange_set.get().old_value, "Completed") + self.assertEqual(followup.ticketchange_set.get().new_value, "To do") task.refresh_from_db() self.assertIsNone(task.completion_date) def test_display_checklist_templates(self): ChecklistTemplate.objects.create( - name='Test checklist template', - task_list=['first', 'second', 'third'] + name="Test checklist template", task_list=["first", "second", "third"] ) - response = self.client.get(reverse('helpdesk:checklist_templates')) + response = self.client.get(reverse("helpdesk:checklist_templates")) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html') - self.assertContains(response, 'Test checklist template') - self.assertContains(response, '3 tasks') + self.assertTemplateUsed(response, "helpdesk/checklist_templates.html") + self.assertContains(response, "Test checklist template") + self.assertContains(response, "3 tasks") def test_create_checklist_template(self): self.assertEqual(ChecklistTemplate.objects.count(), 0) response = self.client.post( - reverse('helpdesk:checklist_templates'), + reverse("helpdesk:checklist_templates"), data={ - 'name': 'Test checklist template', - 'task_list': '["first", "second", "third"]' + "name": "Test checklist template", + "task_list": '["first", "second", "third"]', }, - follow=True + follow=True, ) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html') + self.assertTemplateUsed(response, "helpdesk/checklist_templates.html") self.assertEqual(ChecklistTemplate.objects.count(), 1) checklist_template = ChecklistTemplate.objects.get() - self.assertEqual(checklist_template.name, 'Test checklist template') - self.assertEqual(checklist_template.task_list, ['first', 'second', 'third']) + self.assertEqual(checklist_template.name, "Test checklist template") + self.assertEqual(checklist_template.task_list, ["first", "second", "third"]) def test_edit_checklist_template(self): checklist_template = ChecklistTemplate.objects.create( - name='Test checklist template', - task_list=['first', 'second', 'third'] + name="Test checklist template", task_list=["first", "second", "third"] ) self.assertEqual(ChecklistTemplate.objects.count(), 1) response = self.client.post( - reverse('helpdesk:edit_checklist_template', kwargs={'checklist_template_id': checklist_template.id}), + reverse( + "helpdesk:edit_checklist_template", + kwargs={"checklist_template_id": checklist_template.id}, + ), data={ - 'name': 'New checklist template', - 'task_list': '["new first", "second", "third", "last"]' + "name": "New checklist template", + "task_list": '["new first", "second", "third", "last"]', }, - follow=True + follow=True, ) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html') + self.assertTemplateUsed(response, "helpdesk/checklist_templates.html") self.assertEqual(ChecklistTemplate.objects.count(), 1) checklist_template.refresh_from_db() - self.assertEqual(checklist_template.name, 'New checklist template') - self.assertEqual(checklist_template.task_list, ['new first', 'second', 'third', 'last']) + self.assertEqual(checklist_template.name, "New checklist template") + self.assertEqual( + checklist_template.task_list, ["new first", "second", "third", "last"] + ) def test_delete_checklist_template(self): checklist_template = ChecklistTemplate.objects.create( - name='Test checklist template', - task_list=['first', 'second', 'third'] + name="Test checklist template", task_list=["first", "second", "third"] ) self.assertEqual(ChecklistTemplate.objects.count(), 1) response = self.client.post( - reverse('helpdesk:delete_checklist_template', kwargs={'checklist_template_id': checklist_template.id}), - follow=True + reverse( + "helpdesk:delete_checklist_template", + kwargs={"checklist_template_id": checklist_template.id}, + ), + follow=True, ) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'helpdesk/checklist_templates.html') + self.assertTemplateUsed(response, "helpdesk/checklist_templates.html") self.assertEqual(ChecklistTemplate.objects.count(), 0) diff --git a/helpdesk/tests/test_get_email.py b/helpdesk/tests/test_get_email.py index 4f02ed96..cb7f33a1 100644 --- a/helpdesk/tests/test_get_email.py +++ b/helpdesk/tests/test_get_email.py @@ -14,7 +14,14 @@ import helpdesk.email from helpdesk.email import extract_email_metadata, process_as_attachment from helpdesk.exceptions import DeleteIgnoredTicketException, IgnoreTicketException from helpdesk.management.commands.get_email import Command -from helpdesk.models import FollowUp, FollowUpAttachment, IgnoreEmail, Queue, Ticket, TicketCC +from helpdesk.models import ( + FollowUp, + FollowUpAttachment, + IgnoreEmail, + Queue, + Ticket, + TicketCC, +) from helpdesk.tests import utils import itertools import logging @@ -40,31 +47,37 @@ fake_time = time.time() class GetEmailCommonTests(TestCase): - def setUp(self): - self.queue_public = Queue.objects.create(title='Test', slug='test') - self.logger = logging.getLogger('helpdesk') + self.queue_public = Queue.objects.create(title="Test", slug="test") + self.logger = logging.getLogger("helpdesk") + # 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 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) + with mock.patch.object( + Command, "handle", return_value=None + ) as mocked_handle: + call_command("get_email", quiet=quiet_test_value) mocked_handle.assert_called_once() for _, kwargs in mocked_handle.call_args_list: - self.assertEqual(quiet_test_value, (kwargs['quiet'])) + self.assertEqual(quiet_test_value, (kwargs["quiet"])) def test_email_with_blank_body_and_attachment(self): """ Tests that emails with blank bodies and attachments work. https://github.com/django-helpdesk/django-helpdesk/issues/700 """ - with open(os.path.join(THIS_DIR, "test_files/blank-body-with-attachment.eml"), encoding="utf-8") as fd: + with open( + os.path.join(THIS_DIR, "test_files/blank-body-with-attachment.eml"), + encoding="utf-8", + ) as fd: test_email = fd.read() ticket = helpdesk.email.extract_email_metadata( - test_email, self.queue_public, self.logger) + test_email, self.queue_public, self.logger + ) # title got truncated because of max_lengh of the model.title field assert ticket.title == ( "Attachment without body - and a loooooooooooooooooooooooooooooooooo" @@ -77,31 +90,40 @@ class GetEmailCommonTests(TestCase): """ Tests that emails with quoted-printable bodies work. """ - with open(os.path.join(THIS_DIR, "test_files/quoted_printable.eml"), encoding="utf-8") as fd: + with open( + os.path.join(THIS_DIR, "test_files/quoted_printable.eml"), encoding="utf-8" + ) as fd: test_email = fd.read() ticket = helpdesk.email.extract_email_metadata( - test_email, self.queue_public, self.logger) + 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): """ Tests that emails with 8bit transfer encoding and utf-8 charset https://github.com/django-helpdesk/django-helpdesk/issues/732 """ - with open(os.path.join(THIS_DIR, "test_files/all-special-chars.eml"), encoding="utf-8") as fd: + with open( + os.path.join(THIS_DIR, "test_files/all-special-chars.eml"), encoding="utf-8" + ) as fd: test_email = fd.read() ticket = helpdesk.email.extract_email_metadata( - test_email, self.queue_public, self.logger) + test_email, self.queue_public, self.logger + ) self.assertEqual(ticket.title, "Testovácí email") self.assertEqual(ticket.description, "íářčšáíéřášč") @@ -113,36 +135,43 @@ class GetEmailCommonTests(TestCase): HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL is set to True. Otherwise it should only be in the Followup records Comment field NOTE: This test weirdly tests having completely different content in the HTML part - than the PLAIN part so not sure about the validity of this as a test but + than the PLAIN part so not sure about the validity of this as a test but the code does support this oddity anyway for backwards compatibility """ - with open(os.path.join(THIS_DIR, "test_files/utf-nondecodable.eml"), encoding="utf-8") as fd: + with open( + os.path.join(THIS_DIR, "test_files/utf-nondecodable.eml"), encoding="utf-8" + ) as fd: test_email = fd.read() ticket = helpdesk.email.extract_email_metadata( - test_email, self.queue_public, self.logger) - self.assertEqual( - ticket.title, "Fwd: Cyklozaměstnavatel - změna vyhodnocení") - self.assertIn("prosazuje lepší", ticket.description, "Missing text \"prosazuje lepší\" in description: %s" % ticket.description) + test_email, self.queue_public, self.logger + ) + self.assertEqual(ticket.title, "Fwd: Cyklozaměstnavatel - změna vyhodnocení") + self.assertIn( + "prosazuje lepší", + ticket.description, + 'Missing text "prosazuje lepší" in description: %s' % 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_just_message_stored(self): """ The forwarded part of the message must still be in the ticket description if HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL is set to True. - Otherwise it should only be in the Followup records Comment field + Otherwise it should only be in the Followup records Comment field """ - with open(os.path.join(THIS_DIR, "test_files/forwarded-message.eml"), encoding="utf-8") as fd: + with open( + os.path.join(THIS_DIR, "test_files/forwarded-message.eml"), encoding="utf-8" + ) as fd: test_email = fd.read() ticket = helpdesk.email.extract_email_metadata( - test_email, self.queue_public, self.logger) - self.assertEqual( - ticket.title, "Test with original message from GitHub") + 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) self.assertTrue("Hello there!" in ticket.description, ticket.description) self.assertTrue(FollowUp.objects.filter(ticket=ticket).count() == 1) @@ -152,19 +181,23 @@ class GetEmailCommonTests(TestCase): """ The forwarded part of the message must still be in the ticket description if HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL is set to True. - Otherwise it should only be in the Followup records Comment field + Otherwise it should only be in the Followup records Comment field """ - with open(os.path.join(THIS_DIR, "test_files/forwarded-message.eml"), encoding="utf-8") as fd: + with open( + os.path.join(THIS_DIR, "test_files/forwarded-message.eml"), encoding="utf-8" + ) as fd: test_email = fd.read() ticket = helpdesk.email.extract_email_metadata( - test_email, self.queue_public, self.logger) - self.assertEqual( - ticket.title, "Test with original message from GitHub") + 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) self.assertTrue("Hello there!" not in ticket.description, ticket.description) followups = FollowUp.objects.filter(ticket=ticket).values("comment") self.assertTrue(followups.count() == 1) - self.assertTrue("Hello there!" in followups[0]["comment"], followups[0]["comment"]) + self.assertTrue( + "Hello there!" in followups[0]["comment"], followups[0]["comment"] + ) def test_will_delete_ignored_email(self): """ @@ -172,7 +205,9 @@ class GetEmailCommonTests(TestCase): to ensure the email is deleted """ message, from_meta, _ = utils.generate_text_email(locale="es_ES") - ignore = IgnoreEmail(name="Test Ignore", email_address=from_meta[1], keep_in_mailbox=False) + ignore = IgnoreEmail( + name="Test Ignore", email_address=from_meta[1], keep_in_mailbox=False + ) ignore.save() with self.assertRaises(DeleteIgnoredTicketException): extract_email_metadata(message.as_string(), self.queue_public, self.logger) @@ -183,7 +218,9 @@ class GetEmailCommonTests(TestCase): to ensure the email is NOT deleted """ message, from_meta, _ = utils.generate_text_email(locale="es_ES") - ignore = IgnoreEmail(name="Test Ignore", email_address=from_meta[1], keep_in_mailbox=True) + ignore = IgnoreEmail( + name="Test Ignore", email_address=from_meta[1], keep_in_mailbox=True + ) ignore.save() with self.assertRaises(IgnoreTicketException): extract_email_metadata(message.as_string(), self.queue_public, self.logger) @@ -198,39 +235,49 @@ class GetEmailCommonTests(TestCase): process_as_attachment(part, counter=1, files=files, logger=self.logger) sent_file: SimpleUploadedFile = files[0] # The extractor prepends a part identifier so compare the ending - self.assertTrue(sent_file.name.endswith(filename), f"Filename extracted does not match: {sent_file.name}") + self.assertTrue( + sent_file.name.endswith(filename), + f"Filename extracted does not match: {sent_file.name}", + ) def test_wrong_extension_attachment(self): """ Tests if an attachment with a wrong extension doesn't stop the email process """ - message, _, _ = utils.generate_multipart_email(type_list=['plain', 'executable']) + message, _, _ = utils.generate_multipart_email( + type_list=["plain", "executable"] + ) self.assertEqual(len(mail.outbox), 0) # @UndefinedVariable - with self.assertLogs(logger='helpdesk', level='ERROR') as cm: + with self.assertLogs(logger="helpdesk", level="ERROR") as cm: extract_email_metadata(message.as_string(), self.queue_public, self.logger) self.assertIn( - "ERROR:helpdesk:['Unsupported file extension: .exe']", - cm.output + "ERROR:helpdesk:['Unsupported file extension: .exe']", cm.output ) self.assertEqual(len(mail.outbox), 1) # @UndefinedVariable - self.assertEqual(f'[test-1] {message.get("subject")} (Opened)', mail.outbox[0].subject) + self.assertEqual( + f"[test-1] {message.get('subject')} (Opened)", mail.outbox[0].subject + ) def test_multiple_attachments(self): """ Tests the saving of multiple attachments """ - message, _, _ = utils.generate_multipart_email(type_list=['plain', 'file', 'image']) + message, _, _ = utils.generate_multipart_email( + type_list=["plain", "file", "image"] + ) self.assertEqual(len(mail.outbox), 0) # @UndefinedVariable extract_email_metadata(message.as_string(), self.queue_public, self.logger) self.assertEqual(len(mail.outbox), 1) # @UndefinedVariable - self.assertEqual(f'[test-1] {message.get("subject")} (Opened)', mail.outbox[0].subject) + self.assertEqual( + f"[test-1] {message.get('subject')} (Opened)", mail.outbox[0].subject + ) ticket = Ticket.objects.get() followup = ticket.followup_set.get() @@ -240,16 +287,17 @@ class GetEmailCommonTests(TestCase): """ Tests that a wrong extension won't stop from saving other valid attachment """ - message, _, _ = utils.generate_multipart_email(type_list=['plain', 'executable', 'file', 'executable']) + message, _, _ = utils.generate_multipart_email( + type_list=["plain", "executable", "file", "executable"] + ) self.assertEqual(len(mail.outbox), 0) # @UndefinedVariable - with self.assertLogs(logger='helpdesk', level='ERROR') as cm: + with self.assertLogs(logger="helpdesk", level="ERROR") as cm: extract_email_metadata(message.as_string(), self.queue_public, self.logger) self.assertIn( - "ERROR:helpdesk:['Unsupported file extension: .exe']", - cm.output + "ERROR:helpdesk:['Unsupported file extension: .exe']", cm.output ) ticket = Ticket.objects.get() @@ -260,52 +308,83 @@ class GetEmailCommonTests(TestCase): """ Is a multipart attachment to an email correctly saved as an attachment """ - att_filename = 'email_attachment.eml' - message, _, _ = utils.generate_multipart_email(type_list=['plain', 'html']) - email_attachment, _, _ = utils.generate_multipart_email(type_list=['plain', 'html']) + att_filename = "email_attachment.eml" + message, _, _ = utils.generate_multipart_email(type_list=["plain", "html"]) + email_attachment, _, _ = utils.generate_multipart_email( + type_list=["plain", "html"] + ) att_content = email_attachment.as_string() - message.attach(utils.generate_file_mime_part(filename=att_filename, content=att_content)) + message.attach( + utils.generate_file_mime_part(filename=att_filename, content=att_content) + ) extract_email_metadata(message.as_string(), self.queue_public, self.logger) self.assertEqual(len(mail.outbox), 1) # @UndefinedVariable - self.assertEqual(f'[test-1] {message.get("subject")} (Opened)', mail.outbox[0].subject) + self.assertEqual( + f"[test-1] {message.get('subject')} (Opened)", mail.outbox[0].subject + ) ticket = Ticket.objects.get() followup = ticket.followup_set.get() for att_retrieved in followup.followupattachment_set.all(): - if (helpdesk.email.HTML_EMAIL_ATTACHMENT_FILENAME == att_retrieved.filename): - # Ignore the HTML formatted conntent of the email that is attached + if helpdesk.email.HTML_EMAIL_ATTACHMENT_FILENAME == att_retrieved.filename: + # Ignore the HTML formatted conntent of the email that is attached continue - self.assertTrue(att_retrieved.filename.endswith(att_filename), "Filename of attached multipart not detected: %s" % (att_retrieved.filename)) - with att_retrieved.file.open('r') as f: + self.assertTrue( + att_retrieved.filename.endswith(att_filename), + "Filename of attached multipart not detected: %s" + % (att_retrieved.filename), + ) + with att_retrieved.file.open("r") as f: retrieved_content = f.read() - self.assertEqual(att_content, retrieved_content, "Retrieved attachment content different to original :\n\n%s\n\n%s" % (att_content, retrieved_content)) + self.assertEqual( + att_content, + retrieved_content, + "Retrieved attachment content different to original :\n\n%s\n\n%s" + % (att_content, retrieved_content), + ) def test_email_with_inline_and_multipart_as_attachments(self): """ Test a multipart email with an inline attachment and a multipart email attachment to the email """ - inline_att_filename = 'inline_attachment.jpg' - email_att_filename = 'email_attachment2.eml' + inline_att_filename = "inline_attachment.jpg" + email_att_filename = "email_attachment2.eml" # Create the inline attachment for the email using an ID so we can reference it in the email body inline_image_id = "liTE5er6Dbnd" - inline_attachment = utils.generate_image_mime_part(locale="en_US", imagename=inline_att_filename, disposition_primary_type="inline") - inline_attachment.add_header('X-Attachment-Id', inline_image_id) - inline_attachment.add_header('Content-ID', '<' + inline_image_id + '>') + inline_attachment = utils.generate_image_mime_part( + locale="en_US", + imagename=inline_att_filename, + disposition_primary_type="inline", + ) + inline_attachment.add_header("X-Attachment-Id", inline_image_id) + inline_attachment.add_header("Content-ID", "<" + inline_image_id + ">") # Create the actual email with its plain and HTML parts alt_email_message = MIMEMultipart("alternative") # Create the plain and HTML that will reference the inline attachment - plain_body = "Test with inline image: \n[image: " + inline_att_filename + "]\n\n" + plain_body = ( + "Test with inline image: \n[image: " + inline_att_filename + "]\n\n" + ) plain_msg = MIMEText(plain_body) alt_email_message.attach(plain_msg) - html_body = '
Test with inline image: 3D="'
' + html_body = ( + '
Test with inline image: 3D="'
' + ) html_msg = MIMEText(html_body, "html") alt_email_message.attach(html_msg) # Create the email to be attached and attach that as well - email_to_be_attached, _, _ = utils.generate_multipart_email(type_list=['plain', 'html']) + email_to_be_attached, _, _ = utils.generate_multipart_email( + type_list=["plain", "html"] + ) email_as_attachment = MIMEMessage(email_to_be_attached) - email_as_attachment.add_header('Content-Disposition', 'attachment', filename=email_att_filename) + email_as_attachment.add_header( + "Content-Disposition", "attachment", filename=email_att_filename + ) # Now create the base multipart and attach all the other parts to it related_message = MIMEMultipart("related") related_message.attach(alt_email_message) @@ -313,12 +392,16 @@ class GetEmailCommonTests(TestCase): base_message = MIMEMultipart("mixed") base_message.attach(related_message) base_message.attach(email_as_attachment) - utils.add_simple_email_headers(base_message, locale="en_US", use_short_email=True) + utils.add_simple_email_headers( + base_message, locale="en_US", use_short_email=True + ) # Now send the part to the email workflow extract_email_metadata(base_message.as_string(), self.queue_public, self.logger) self.assertEqual(len(mail.outbox), 1) # @UndefinedVariable - self.assertEqual(f'[test-1] {base_message.get("subject")} (Opened)', mail.outbox[0].subject) + self.assertEqual( + f"[test-1] {base_message.get('subject')} (Opened)", mail.outbox[0].subject + ) ticket = Ticket.objects.get() followup = ticket.followup_set.get() @@ -326,129 +409,154 @@ class GetEmailCommonTests(TestCase): inline_found = False email_attachment_found = False for att_retrieved in followup.followupattachment_set.all(): - if (helpdesk.email.HTML_EMAIL_ATTACHMENT_FILENAME == att_retrieved.filename): - # Ignore the HTML formatted content of the email that is attached + if helpdesk.email.HTML_EMAIL_ATTACHMENT_FILENAME == att_retrieved.filename: + # Ignore the HTML formatted content of the email that is attached continue if att_retrieved.filename.endswith(inline_att_filename): inline_found = True - elif att_retrieved.filename.endswith(email_att_filename): + elif att_retrieved.filename.endswith(email_att_filename): email_attachment_found = True else: - self.assertTrue(False, "Unexpected file in ticket attachments: %s" % att_retrieved.filename) - self.assertTrue(email_attachment_found, "Email attachment file not found ticket attachments: %s" % (email_att_filename)) - self.assertTrue(inline_found, "Inline file not found in email: %s" % (inline_att_filename)) + self.assertTrue( + False, + "Unexpected file in ticket attachments: %s" + % att_retrieved.filename, + ) + self.assertTrue( + email_attachment_found, + "Email attachment file not found ticket attachments: %s" + % (email_att_filename), + ) + self.assertTrue( + inline_found, "Inline file not found in email: %s" % (inline_att_filename) + ) - def test_email_with_txt_as_attachment_with_simple_alternative_message(self): """ Test an email with a txt extension email attachment to the email where the message part of the email is in a multipart/alternative directly off the main multipart/mixed (no multipart/related) """ - email_att_filename = 'the_quick_brown_fox.txt' + email_att_filename = "the_quick_brown_fox.txt" # Create the actual email with its plain and HTML parts alt_email_message = MIMEMultipart("alternative") # Create the plain and HTML that will reference the inline attachment plain_body = "Is wet birds attached?\n\n" plain_msg = MIMEText(plain_body) alt_email_message.attach(plain_msg) - html_body = '
Is wet birds attached?

' + html_body = "
Is wet birds attached?

" html_msg = MIMEText(html_body, "html") alt_email_message.attach(html_msg) # Now create the base multipart and attach all the other parts to it base_message = MIMEMultipart("mixed") base_message.attach(alt_email_message) email_attachment = MIMEText("Wet birds don't fly at night.") - email_attachment.add_header('Content-Disposition', 'attachment', filename=email_att_filename) + email_attachment.add_header( + "Content-Disposition", "attachment", filename=email_att_filename + ) base_message.attach(email_attachment) - utils.add_simple_email_headers(base_message, locale="en_US", use_short_email=True) + utils.add_simple_email_headers( + base_message, locale="en_US", use_short_email=True + ) # Now send the part to the email workflow extract_email_metadata(base_message.as_string(), self.queue_public, self.logger) self.assertEqual(len(mail.outbox), 1) # @UndefinedVariable - #self.assertEqual(f'[test-1] {base_message.get("subject")} (Opened)', mail.outbox[0].subject) + # self.assertEqual(f'[test-1] {base_message.get("subject")} (Opened)', mail.outbox[0].subject) ticket = Ticket.objects.get() followup = ticket.followup_set.get() # Check attachment is stored as attached file email_attachment_found = False for att_retrieved in followup.followupattachment_set.all(): - if (helpdesk.email.HTML_EMAIL_ATTACHMENT_FILENAME == att_retrieved.filename): - # Ignore the HTML formatted content of the email that is attached + if helpdesk.email.HTML_EMAIL_ATTACHMENT_FILENAME == att_retrieved.filename: + # Ignore the HTML formatted content of the email that is attached continue - if att_retrieved.filename.endswith(email_att_filename): + if att_retrieved.filename.endswith(email_att_filename): email_attachment_found = True else: - self.assertTrue(False, "Unexpected file in ticket attachments: %s" % att_retrieved.filename) - self.assertTrue(email_attachment_found, "Email attachment file not found ticket attachments: %s" % (email_att_filename)) + self.assertTrue( + False, + "Unexpected file in ticket attachments: %s" + % att_retrieved.filename, + ) + self.assertTrue( + email_attachment_found, + "Email attachment file not found ticket attachments: %s" + % (email_att_filename), + ) class EmailTaskTests(TestCase): - def setUp(self): self.num_queues = 5 self.exception_queues = [2, 4] self.q_ids = [] for i in range(self.num_queues): q = Queue.objects.create( - title=f'Test{i+1}', - slug=f'test{i+1}', - email_box_type='local', - allow_email_submission=True - ) + title=f"Test{i + 1}", + slug=f"test{i + 1}", + email_box_type="local", + allow_email_submission=True, + ) self.q_ids.append(q.id) - self.logger = logging.getLogger('helpdesk') + self.logger = logging.getLogger("helpdesk") def test_get_email_with_debug_to_stdout_option(self): - """Test debug_to_stdout option """ + """Test debug_to_stdout option""" # Test get_email with debug_to_stdout set to True and also False, and verify # handle receives debug_to_stdout option set properly for debug_to_stdout in [True, False]: - with mock.patch.object(Command, 'handle', return_value=None) as mocked_handle: - call_command('get_email', "--debug_to_stdout") if debug_to_stdout else call_command('get_email') + with mock.patch.object( + Command, "handle", return_value=None + ) as mocked_handle: + call_command( + "get_email", "--debug_to_stdout" + ) if debug_to_stdout else call_command("get_email") mocked_handle.assert_called_once() for _, kwargs in mocked_handle.call_args_list: - self.assertEqual(debug_to_stdout, (kwargs['debug_to_stdout'])) + self.assertEqual(debug_to_stdout, (kwargs["debug_to_stdout"])) - @patch('helpdesk.email.process_queue') + @patch("helpdesk.email.process_queue") def test_get_email_with_queue_failure(self, mocked_process_queue): """Test all queues are processed if specified queues have exceptions""" ret_values = [ - Exception(f"Error Q{i};") if (i in self.exception_queues) else None for i in range(1, self.num_queues+1) - ] + Exception(f"Error Q{i};") if (i in self.exception_queues) else None + for i in range(1, self.num_queues + 1) + ] mocked_process_queue.side_effect = ret_values call_command( - 'get_email', - '--debug_to_stdout', - ) + "get_email", + "--debug_to_stdout", + ) self.assertEqual(mocked_process_queue.call_count, self.num_queues) not_processed_count = Queue.objects.filter( email_box_type__isnull=False, allow_email_submission=True, - email_box_last_check__isnull=True).count() + email_box_last_check__isnull=True, + ).count() self.assertEqual( not_processed_count, len(self.exception_queues), - "Incorrect number of queues that did not get processed due to a forced exception." - ) + "Incorrect number of queues that did not get processed due to a forced exception.", + ) class GetEmailParametricTemplate(object): """TestCase that checks basic email functionality across methods and socks configs.""" def setUp(self): - self.temp_logdir = mkdtemp() kwargs = { - "title": 'Basic Queue', - "slug": 'QQ', + "title": "Basic Queue", + "slug": "QQ", "allow_public_submission": True, "allow_email_submission": True, "email_box_type": self.method, "logging_dir": self.temp_logdir, - "logging_type": 'none' + "logging_type": "none", } - if self.method == 'local': - kwargs["email_box_local_dir"] = '/var/lib/mail/helpdesk/' + if self.method == "local": + kwargs["email_box_local_dir"] = "/var/lib/mail/helpdesk/" else: kwargs["email_box_host"] = unrouted_email_server kwargs["email_box_port"] = unused_port @@ -461,77 +569,86 @@ class GetEmailParametricTemplate(object): self.queue_public = Queue.objects.create(**kwargs) self.token = { - 'token_type': 'Bearer', - 'access_token': 'asdfoiw37850234lkjsdfsdf', - 'refresh_token': 'sldvafkjw34509s8dfsdf', - 'expires_in': '3600', - 'expires_at': fake_time + 3600, + "token_type": "Bearer", + "access_token": "asdfoiw37850234lkjsdfsdf", + "refresh_token": "sldvafkjw34509s8dfsdf", + "expires_in": "3600", + "expires_at": fake_time + 3600, } - self.client_id = 'foo' + self.client_id = "foo" self.client = BackendApplicationClient(self.client_id) - def tearDown(self): - rmtree(self.temp_logdir) def test_read_plain_email(self): """Tests reading plain text emails from a queue and creating tickets. - For each email source supported, we mock the backend to provide - authentically formatted responses containing our test data.""" + 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/ 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: from socks import ProxyConnectionError - with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): - call_command('get_email') + + with self.assertRaisesRegex( + ProxyConnectionError, "%s:%s" % (unrouted_socks_server, unused_port) + ): + call_command("get_email") else: # Test local email reading - if self.method == 'local': - with mock.patch('os.listdir') as mocked_listdir, \ - mock.patch('helpdesk.email.isfile') as mocked_isfile, \ - mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ - mock.patch('os.unlink'): + if self.method == "local": + with ( + mock.patch("os.listdir") as mocked_listdir, + mock.patch("helpdesk.email.isfile") as mocked_isfile, + mock.patch("builtins.open", mock.mock_open(read_data=test_email)), + mock.patch("os.unlink"), + ): mocked_isfile.return_value = True - mocked_listdir.return_value = ['filename1', 'filename2'] + mocked_listdir.return_value = ["filename1", "filename2"] - call_command('get_email') + 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': + elif self.method == "pop3": # 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')), + "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.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) - call_command('get_email') + 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) + call_command("get_email") - elif self.method == 'imap': + elif self.method == "imap": # mock imaplib.IMAP4's search and fetch methods with responses # from RFC 3501 imap_emails = { @@ -540,18 +657,19 @@ class GetEmailParametricTemplate(object): } 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]) - with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: - mocked_imaplib.IMAP4 = mock.Mock( - return_value=mocked_imaplib_server) - call_command('get_email') + 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) + call_command("get_email") - elif self.method == 'oauth': + elif self.method == "oauth": # mock the oauthlib session and requests oauth backendclient # then mock imaplib.IMAP4's search and fetch methods with responses # from RFC 3501 @@ -561,32 +679,39 @@ class GetEmailParametricTemplate(object): } 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]) + side_effect=lambda x, _: imap_emails[x] + ) mocked_oauth_backend_client = mock.Mock() - with mock.patch('helpdesk.email.oauth2lib', autospec=True) as mocked_oauth2lib: + with mock.patch( + "helpdesk.email.oauth2lib", autospec=True + ) as mocked_oauth2lib: mocked_oauth2lib.BackendApplicationClient = mock.Mock( - return_value=mocked_oauth_backend_client) - - mocked_oauth_session = mock.Mock() - mocked_oauth_session.fetch_token = mock.Mock( - return_value={} + return_value=mocked_oauth_backend_client ) - with mock.patch('helpdesk.email.requests_oauthlib', autospec=True) as mocked_requests_oauthlib: + mocked_oauth_session = mock.Mock() + mocked_oauth_session.fetch_token = mock.Mock(return_value={}) + + with mock.patch( + "helpdesk.email.requests_oauthlib", autospec=True + ) as mocked_requests_oauthlib: mocked_requests_oauthlib.OAuth2Session = mock.Mock( - return_value=mocked_oauth_session) + return_value=mocked_oauth_session + ) - with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: + with mock.patch( + "helpdesk.email.imaplib", autospec=True + ) as mocked_imaplib: mocked_imaplib.IMAP4 = mock.Mock( - return_value=mocked_imaplib_server) + return_value=mocked_imaplib_server + ) - call_command('get_email') + call_command("get_email") ticket1 = get_object_or_404(Ticket, pk=1) self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) @@ -600,60 +725,73 @@ class GetEmailParametricTemplate(object): def test_commas_in_mail_headers(self): """Tests correctly decoding mail headers when a comma is encoded into - UTF-8. See bug report #832.""" + UTF-8. See bug report #832.""" # Create the from using standard RFC required formats # Override the last_name to ensure we get a non-ascii character in it - test_email_from_meta = utils.generate_email_address("fr_FR", last_name_override="Bouissières") + test_email_from_meta = utils.generate_email_address( + "fr_FR", last_name_override="Bouissières" + ) 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_meta[0] + \ - "\nSubject: " + test_email_subject + "\n\n" + test_email_body + test_email = ( + "To: helpdesk@example.com\nFrom: " + + test_email_from_meta[0] + + "\nSubject: " + + test_email_subject + + "\n\n" + + test_email_body + ) test_mail_len = len(test_email) if self.socks: from socks import ProxyConnectionError - with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): - call_command('get_email') + + with self.assertRaisesRegex( + ProxyConnectionError, "%s:%s" % (unrouted_socks_server, unused_port) + ): + call_command("get_email") else: # Test local email reading - if self.method == 'local': - with mock.patch('os.listdir') as mocked_listdir, \ - mock.patch('helpdesk.email.isfile') as mocked_isfile, \ - mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ - mock.patch('os.unlink'): + if self.method == "local": + with ( + mock.patch("os.listdir") as mocked_listdir, + mock.patch("helpdesk.email.isfile") as mocked_isfile, + mock.patch("builtins.open", mock.mock_open(read_data=test_email)), + mock.patch("os.unlink"), + ): mocked_isfile.return_value = True - mocked_listdir.return_value = ['filename1', 'filename2'] + mocked_listdir.return_value = ["filename1", "filename2"] - call_command('get_email') + 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': + elif self.method == "pop3": # 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')), + "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.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) - call_command('get_email') + 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) + call_command("get_email") - elif self.method == 'imap': + elif self.method == "imap": # mock imaplib.IMAP4's search and fetch methods with responses # from RFC 3501 imap_emails = { @@ -662,18 +800,19 @@ class GetEmailParametricTemplate(object): } 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]) - with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: - mocked_imaplib.IMAP4 = mock.Mock( - return_value=mocked_imaplib_server) - call_command('get_email') + 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) + call_command("get_email") - elif self.method == 'oauth': + elif self.method == "oauth": # mock the oauthlib session and requests oauth backendclient # then mock imaplib.IMAP4's search and fetch methods with responses # from RFC 3501 @@ -683,32 +822,39 @@ class GetEmailParametricTemplate(object): } 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]) + side_effect=lambda x, _: imap_emails[x] + ) mocked_oauth_backend_client = mock.Mock() - with mock.patch('helpdesk.email.oauth2lib', autospec=True) as mocked_oauth2lib: + with mock.patch( + "helpdesk.email.oauth2lib", autospec=True + ) as mocked_oauth2lib: mocked_oauth2lib.BackendApplicationClient = mock.Mock( - return_value=mocked_oauth_backend_client) - - mocked_oauth_session = mock.Mock() - mocked_oauth_session.fetch_token = mock.Mock( - return_value={} + return_value=mocked_oauth_backend_client ) - with mock.patch('helpdesk.email.requests_oauthlib', autospec=True) as mocked_requests_oauthlib: + mocked_oauth_session = mock.Mock() + mocked_oauth_session.fetch_token = mock.Mock(return_value={}) + + with mock.patch( + "helpdesk.email.requests_oauthlib", autospec=True + ) as mocked_requests_oauthlib: mocked_requests_oauthlib.OAuth2Session = mock.Mock( - return_value=mocked_oauth_session) + return_value=mocked_oauth_session + ) - with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: + with mock.patch( + "helpdesk.email.imaplib", autospec=True + ) as mocked_imaplib: mocked_imaplib.IMAP4 = mock.Mock( - return_value=mocked_imaplib_server) + return_value=mocked_imaplib_server + ) - call_command('get_email') + call_command("get_email") ticket1 = get_object_or_404(Ticket, pk=1) self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) @@ -724,62 +870,75 @@ class GetEmailParametricTemplate(object): def test_read_email_with_template_tag(self): """Tests reading plain text emails from a queue and creating tickets, - except this time the email body contains a Django template tag. - For each email source supported, we mock the backend to provide - authentically formatted responses containing our test data.""" + except this time the email body contains a Django template tag. + 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/ 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_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_mail_len = len(test_email) if self.socks: from socks import ProxyConnectionError - with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): - call_command('get_email') + + with self.assertRaisesRegex( + ProxyConnectionError, "%s:%s" % (unrouted_socks_server, unused_port) + ): + call_command("get_email") else: # Test local email reading - if self.method == 'local': - with mock.patch('os.listdir') as mocked_listdir, \ - mock.patch('helpdesk.email.isfile') as mocked_isfile, \ - mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ - mock.patch('os.unlink'): + if self.method == "local": + with ( + mock.patch("os.listdir") as mocked_listdir, + mock.patch("helpdesk.email.isfile") as mocked_isfile, + mock.patch("builtins.open", mock.mock_open(read_data=test_email)), + mock.patch("os.unlink"), + ): mocked_isfile.return_value = True - mocked_listdir.return_value = ['filename1', 'filename2'] + mocked_listdir.return_value = ["filename1", "filename2"] - call_command('get_email') + 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': + elif self.method == "pop3": # 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')), + "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.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) - call_command('get_email') + 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) + call_command("get_email") - elif self.method == 'imap': + elif self.method == "imap": # mock imaplib.IMAP4's search and fetch methods with responses # from RFC 3501 imap_emails = { @@ -788,18 +947,19 @@ class GetEmailParametricTemplate(object): } 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]) - with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: - mocked_imaplib.IMAP4 = mock.Mock( - return_value=mocked_imaplib_server) - call_command('get_email') + 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) + call_command("get_email") - elif self.method == 'oauth': + elif self.method == "oauth": # mock the oauthlib session and requests oauth backendclient # then mock imaplib.IMAP4's search and fetch methods with responses # from RFC 3501 @@ -809,32 +969,39 @@ class GetEmailParametricTemplate(object): } 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]) + side_effect=lambda x, _: imap_emails[x] + ) mocked_oauth_backend_client = mock.Mock() - with mock.patch('helpdesk.email.oauth2lib', autospec=True) as mocked_oauth2lib: + with mock.patch( + "helpdesk.email.oauth2lib", autospec=True + ) as mocked_oauth2lib: mocked_oauth2lib.BackendApplicationClient = mock.Mock( - return_value=mocked_oauth_backend_client) - - mocked_oauth_session = mock.Mock() - mocked_oauth_session.fetch_token = mock.Mock( - return_value={} + return_value=mocked_oauth_backend_client ) - with mock.patch('helpdesk.email.requests_oauthlib', autospec=True) as mocked_requests_oauthlib: + mocked_oauth_session = mock.Mock() + mocked_oauth_session.fetch_token = mock.Mock(return_value={}) + + with mock.patch( + "helpdesk.email.requests_oauthlib", autospec=True + ) as mocked_requests_oauthlib: mocked_requests_oauthlib.OAuth2Session = mock.Mock( - return_value=mocked_oauth_session) + return_value=mocked_oauth_session + ) - with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: + with mock.patch( + "helpdesk.email.imaplib", autospec=True + ) as mocked_imaplib: mocked_imaplib.IMAP4 = mock.Mock( - return_value=mocked_imaplib_server) + return_value=mocked_imaplib_server + ) - call_command('get_email') + call_command("get_email") ticket1 = get_object_or_404(Ticket, pk=1) self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) @@ -848,9 +1015,9 @@ class GetEmailParametricTemplate(object): def test_read_html_multipart_email(self): """Tests reading multipart MIME (HTML body and plain text alternative) - emails from a queue and creating tickets. - For each email source supported, we mock the backend to provide - authentically formatted responses containing our test data.""" + emails from a queue and creating tickets. + 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 me = "my@example.com" @@ -864,11 +1031,11 @@ class GetEmailParametricTemplate(object): subject = "Link" # Create message container - the correct MIME type is # multipart/alternative. - msg = MIMEMultipart('alternative') - msg['Subject'] = subject - msg['From'] = me - msg['To'] = you - msg['Cc'] = cc + msg = MIMEMultipart("alternative") + msg["Subject"] = subject + msg["From"] = me + msg["To"] = you + msg["Cc"] = cc # Create the body of the message (a plain-text and an HTML version). text = "Hi!\nHow are you?\nHere is the link you wanted:\nhttps://www.python.org" html = """\ @@ -883,8 +1050,8 @@ class GetEmailParametricTemplate(object): """ # Record the MIME types of both parts - text/plain and text/html. - part1 = MIMEText(text, 'plain') - part2 = MIMEText(html, 'html') + part1 = MIMEText(text, "plain") + part2 = MIMEText(html, "html") # Attach parts into message container. # According to RFC 2046, the last part of a multipart message, in this case # the HTML message, is best and preferred. @@ -895,49 +1062,55 @@ class GetEmailParametricTemplate(object): if self.socks: from socks import ProxyConnectionError - with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): - call_command('get_email') + + with self.assertRaisesRegex( + ProxyConnectionError, "%s:%s" % (unrouted_socks_server, unused_port) + ): + call_command("get_email") else: # Test local email reading - if self.method == 'local': - with mock.patch('os.listdir') as mocked_listdir, \ - mock.patch('helpdesk.email.isfile') as mocked_isfile, \ - mock.patch('builtins.open', mock.mock_open(read_data=msg.as_string())), \ - mock.patch('os.unlink'): + if self.method == "local": + with ( + mock.patch("os.listdir") as mocked_listdir, + mock.patch("helpdesk.email.isfile") as mocked_isfile, + mock.patch( + "builtins.open", mock.mock_open(read_data=msg.as_string()) + ), + mock.patch("os.unlink"), + ): mocked_isfile.return_value = True - mocked_listdir.return_value = ['filename1', 'filename2'] + mocked_listdir.return_value = ["filename1", "filename2"] - call_command('get_email') + 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': + elif self.method == "pop3": # 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')), + "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.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) - call_command('get_email') + 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) + call_command("get_email") - - elif self.method == 'imap': + elif self.method == "imap": # mock imaplib.IMAP4's search and fetch methods with responses # from RFC 3501 imap_emails = { @@ -946,18 +1119,19 @@ class GetEmailParametricTemplate(object): } 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]) - with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: - mocked_imaplib.IMAP4 = mock.Mock( - return_value=mocked_imaplib_server) - call_command('get_email') + 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) + call_command("get_email") - elif self.method == 'oauth': + elif self.method == "oauth": # mock the oauthlib session and requests oauth backendclient # then mock imaplib.IMAP4's search and fetch methods with responses # from RFC 3501 @@ -967,32 +1141,39 @@ class GetEmailParametricTemplate(object): } 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]) + side_effect=lambda x, _: imap_emails[x] + ) mocked_oauth_backend_client = mock.Mock() - with mock.patch('helpdesk.email.oauth2lib', autospec=True) as mocked_oauth2lib: + with mock.patch( + "helpdesk.email.oauth2lib", autospec=True + ) as mocked_oauth2lib: mocked_oauth2lib.BackendApplicationClient = mock.Mock( - return_value=mocked_oauth_backend_client) - - mocked_oauth_session = mock.Mock() - mocked_oauth_session.fetch_token = mock.Mock( - return_value={} + return_value=mocked_oauth_backend_client ) - with mock.patch('helpdesk.email.requests_oauthlib', autospec=True) as mocked_requests_oauthlib: + mocked_oauth_session = mock.Mock() + mocked_oauth_session.fetch_token = mock.Mock(return_value={}) + + with mock.patch( + "helpdesk.email.requests_oauthlib", autospec=True + ) as mocked_requests_oauthlib: mocked_requests_oauthlib.OAuth2Session = mock.Mock( - return_value=mocked_oauth_session) + return_value=mocked_oauth_session + ) - with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: + with mock.patch( + "helpdesk.email.imaplib", autospec=True + ) as mocked_imaplib: mocked_imaplib.IMAP4 = mock.Mock( - return_value=mocked_imaplib_server) + return_value=mocked_imaplib_server + ) - call_command('get_email') + call_command("get_email") ticket1 = get_object_or_404(Ticket, pk=1) self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) @@ -1004,7 +1185,7 @@ class GetEmailParametricTemplate(object): self.assertEqual(followup1.ticket.id, 1) attach1 = get_object_or_404(FollowUpAttachment, pk=1) self.assertEqual(attach1.followup.id, 1) - self.assertEqual(attach1.filename, 'email_html_body.html') + self.assertEqual(attach1.filename, "email_html_body.html") cc0 = get_object_or_404(TicketCC, pk=1) self.assertEqual(cc0.email, you) cc1 = get_object_or_404(TicketCC, pk=2) @@ -1023,11 +1204,11 @@ class GetEmailParametricTemplate(object): self.assertEqual(followup2.ticket.id, 2) attach2 = get_object_or_404(FollowUpAttachment, pk=2) self.assertEqual(attach2.followup.id, 2) - self.assertEqual(attach2.filename, 'email_html_body.html') + self.assertEqual(attach2.filename, "email_html_body.html") def test_read_pgp_signed_email(self): """Tests reading a PGP signed email to ensure we handle base64 - and PGP signatures appropriately.""" + and PGP signatures appropriately.""" # example email text from #567 on GitHub with open(os.path.join(THIS_DIR, "test_files/pgp.eml"), encoding="utf-8") as fd: test_email = fd.read() @@ -1035,45 +1216,48 @@ class GetEmailParametricTemplate(object): if self.socks: from socks import ProxyConnectionError - with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): - call_command('get_email') + + with self.assertRaisesRegex( + ProxyConnectionError, "%s:%s" % (unrouted_socks_server, unused_port) + ): + call_command("get_email") else: # Test local email reading - if self.method == 'local': - with mock.patch('os.listdir') as mocked_listdir, \ - mock.patch('helpdesk.email.isfile') as mocked_isfile, \ - mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ - mock.patch('os.unlink'): + if self.method == "local": + with ( + mock.patch("os.listdir") as mocked_listdir, + mock.patch("helpdesk.email.isfile") as mocked_isfile, + mock.patch("builtins.open", mock.mock_open(read_data=test_email)), + mock.patch("os.unlink"), + ): mocked_isfile.return_value = True - mocked_listdir.return_value = ['filename1'] + mocked_listdir.return_value = ["filename1"] - call_command('get_email') + 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': + elif self.method == "pop3": # mock poplib.POP3's list and retr methods to provide responses # as per RFC 1939 pop3_emails = { - '1': ("+OK", test_email.split('\n')), + "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.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) - call_command('get_email') + 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) + call_command("get_email") - - elif self.method == 'imap': + elif self.method == "imap": # mock imaplib.IMAP4's search and fetch methods with responses # from RFC 3501 imap_emails = { @@ -1081,18 +1265,19 @@ class GetEmailParametricTemplate(object): } 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]) - with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: - mocked_imaplib.IMAP4 = mock.Mock( - return_value=mocked_imaplib_server) - call_command('get_email') + 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) + call_command("get_email") - elif self.method == 'oauth': + elif self.method == "oauth": # mock the oauthlib session and requests oauth backendclient # then mock imaplib.IMAP4's search and fetch methods with responses # from RFC 3501 @@ -1101,46 +1286,58 @@ class GetEmailParametricTemplate(object): } 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]) + side_effect=lambda x, _: imap_emails[x] + ) mocked_oauth_backend_client = mock.Mock() - with mock.patch('helpdesk.email.oauth2lib', autospec=True) as mocked_oauth2lib: + with mock.patch( + "helpdesk.email.oauth2lib", autospec=True + ) as mocked_oauth2lib: mocked_oauth2lib.BackendApplicationClient = mock.Mock( - return_value=mocked_oauth_backend_client) - - mocked_oauth_session = mock.Mock() - mocked_oauth_session.fetch_token = mock.Mock( - return_value={} + return_value=mocked_oauth_backend_client ) - with mock.patch('helpdesk.email.requests_oauthlib', autospec=True) as mocked_requests_oauthlib: + mocked_oauth_session = mock.Mock() + mocked_oauth_session.fetch_token = mock.Mock(return_value={}) + + with mock.patch( + "helpdesk.email.requests_oauthlib", autospec=True + ) as mocked_requests_oauthlib: mocked_requests_oauthlib.OAuth2Session = mock.Mock( - return_value=mocked_oauth_session) + return_value=mocked_oauth_session + ) - with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: + with mock.patch( + "helpdesk.email.imaplib", autospec=True + ) as mocked_imaplib: mocked_imaplib.IMAP4 = mock.Mock( - return_value=mocked_imaplib_server) + return_value=mocked_imaplib_server + ) - call_command('get_email') + 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") + 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""") + 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) attach1 = get_object_or_404(FollowUpAttachment, pk=1) self.assertEqual(attach1.followup.id, 1) - self.assertEqual(attach1.filename, 'part-1_signature.asc') - self.assertEqual(attach1.file.read(), b"""-----BEGIN PGP SIGNATURE----- + self.assertEqual(attach1.filename, "part-1_signature.asc") + self.assertEqual( + attach1.file.read(), + b"""-----BEGIN PGP SIGNATURE----- iQIcBAEBCAAGBQJaA3dnAAoJELBLc7QPITnLN54P/3Zsu7+AIQWDFTvziJfCqswG u99fG+iWa6ER+iuZG0YU1BdIxIjSKt1pvqB0yXITlT9FCdf1zc0pmeJ08I0a5pVa @@ -1156,9 +1353,12 @@ W7tXhGjMoUvqAxiKkmG3UHFqN4k3EYo13PwoOWyJHD1M9ArbX/Sk9l8DDguCh3DW a9eiiQ+3V1v+7wWHXCzq =6JeP -----END PGP SIGNATURE----- -""") +""", + ) # should this be 'application/pgp-signature'? # self.assertEqual(attach1.mime_type, 'text/plain') + + class GetEmailCCHandling(TestCase): """TestCase that checks CC handling in email. Needs its own test harness.""" @@ -1166,62 +1366,62 @@ class GetEmailCCHandling(TestCase): self.temp_logdir = mkdtemp() kwargs = { - "title": 'CC Queue', - "slug": 'CC', + "title": "CC Queue", + "slug": "CC", "allow_public_submission": True, "allow_email_submission": True, - "email_address": 'queue@example.com', - "email_box_type": 'local', - "email_box_local_dir": '/var/lib/mail/helpdesk/', + "email_address": "queue@example.com", + "email_box_type": "local", + "email_box_local_dir": "/var/lib/mail/helpdesk/", "logging_dir": self.temp_logdir, - "logging_type": 'none' + "logging_type": "none", } self.queue_public = Queue.objects.create(**kwargs) user1_kwargs = { - 'username': 'staff', - 'email': 'staff@example.com', - 'password': make_password('Test1234'), - 'is_staff': True, - 'is_superuser': False, - 'is_active': True + "username": "staff", + "email": "staff@example.com", + "password": make_password("Test1234"), + "is_staff": True, + "is_superuser": False, + "is_active": True, } self.staff_user = User.objects.create(**user1_kwargs) user2_kwargs = { - 'username': 'assigned', - 'email': 'assigned@example.com', - 'password': make_password('Test1234'), - 'is_staff': True, - 'is_superuser': False, - 'is_active': True + "username": "assigned", + "email": "assigned@example.com", + "password": make_password("Test1234"), + "is_staff": True, + "is_superuser": False, + "is_active": True, } self.assigned_user = User.objects.create(**user2_kwargs) user3_kwargs = { - 'username': 'observer', - 'email': 'observer@example.com', - 'password': make_password('Test1234'), - 'is_staff': True, - 'is_superuser': False, - 'is_active': True + "username": "observer", + "email": "observer@example.com", + "password": make_password("Test1234"), + "is_staff": True, + "is_superuser": False, + "is_active": True, } self.observer_user = User.objects.create(**user3_kwargs) ticket_kwargs = { - 'title': 'Original Ticket', - 'queue': self.queue_public, - 'submitter_email': 'submitter@example.com', - 'assigned_to': self.assigned_user, - 'status': 1 + "title": "Original Ticket", + "queue": self.queue_public, + "submitter_email": "submitter@example.com", + "assigned_to": self.assigned_user, + "status": 1, } self.original_ticket = Ticket.objects.create(**ticket_kwargs) cc_kwargs = { - 'ticket': self.original_ticket, - 'user': self.staff_user, - 'can_view': True, - 'can_update': True + "ticket": self.original_ticket, + "user": self.staff_user, + "can_view": True, + "can_update": True, } self.original_cc = TicketCC.objects.create(**cc_kwargs) @@ -1230,7 +1430,7 @@ class GetEmailCCHandling(TestCase): def test_read_email_cc(self): """Tests reading plain text emails from a queue and adding to a ticket, - particularly to test appropriate handling of CC'd emails.""" + particularly to test appropriate handling of CC'd emails.""" # first, check that test ticket exists ticket1 = get_object_or_404(Ticket, pk=1) self.assertEqual(ticket1.ticket_for_url, "CC-1") @@ -1238,9 +1438,8 @@ class GetEmailCCHandling(TestCase): # only the staff_user is CC'd for now 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(ccstaff.user, User.objects.get(username="staff")) + 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/ test_email_from = "submitter@example.com" @@ -1254,28 +1453,50 @@ 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 + ) - with mock.patch('os.listdir') as mocked_listdir, \ - mock.patch('helpdesk.email.isfile') as mocked_isfile, \ - mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ - mock.patch('os.unlink'): + with ( + mock.patch("os.listdir") as mocked_listdir, + mock.patch("helpdesk.email.isfile") as mocked_isfile, + mock.patch("builtins.open", mock.mock_open(read_data=test_email)), + mock.patch("os.unlink"), + ): mocked_isfile.return_value = True - mocked_listdir.return_value = ['filename1'] + mocked_listdir.return_value = ["filename1"] - call_command('get_email') + 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") # 9 unique email addresses are CC'd when all is done self.assertEqual(len(TicketCC.objects.filter(ticket=1)), 9) # next we make sure no duplicates were added, and the # staff users nor submitter were not re-added as email TicketCCs cc1 = get_object_or_404(TicketCC, pk=1) - self.assertEqual(cc1.user, User.objects.get(username='staff')) + self.assertEqual(cc1.user, User.objects.get(username="staff")) cc2 = get_object_or_404(TicketCC, pk=2) self.assertEqual(cc2.email, "alice@example.com") cc3 = get_object_or_404(TicketCC, pk=3) @@ -1291,10 +1512,12 @@ class GetEmailCCHandling(TestCase): cc8 = get_object_or_404(TicketCC, pk=8) self.assertEqual(cc8.email, "submitter@example.com") cc9 = get_object_or_404(TicketCC, pk=9) - self.assertEqual(cc9.user, User.objects.get(username='observer')) + self.assertEqual(cc9.user, User.objects.get(username="observer")) self.assertEqual(cc9.email, "observer@example.com") + + # build matrix of test cases -case_methods = [c[0] for c in Queue._meta.get_field('email_box_type').choices] +case_methods = [c[0] for c in Queue._meta.get_field("email_box_type").choices] # uncomment if you want to run tests with socks - which is much slover # case_socks = [False] + [c[0] for c in Queue._meta.get_field('socks_proxy_type').choices] case_socks = [False] @@ -1302,16 +1525,17 @@ case_matrix = list(itertools.product(case_methods, case_socks)) # Populate TestCases from the matrix of parameters thismodule = sys.modules[__name__] for method, socks in case_matrix: - if method == "local" and socks: continue socks_str = "Nosocks" if socks: socks_str = socks.capitalize() - test_name = str( - "TestGetEmail%s%s" % (method.capitalize(), socks_str)) + 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 4805db34..e7e4ba89 100644 --- a/helpdesk/tests/test_kb.py +++ b/helpdesk/tests/test_kb.py @@ -37,52 +37,52 @@ class KBTests(TestCase): self.user = get_staff_user() def test_kb_index(self): - response = self.client.get(reverse('helpdesk:kb_index')) - self.assertContains(response, 'This is a test category') + response = self.client.get(reverse("helpdesk:kb_index")) + self.assertContains(response, "This is a test category") def test_kb_category(self): - 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", ))) + 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.assertContains(response, '') - self.assertContains(response, '0 open tickets') + self.assertContains(response, "0 open tickets") ticket = Ticket.objects.create( title="Test ticket", queue=self.queue, kbitem=self.kbitem1, ) ticket.save() - response = self.client.get( - reverse('helpdesk:kb_category', args=("test_cat",))) - self.assertContains(response, '1 open tickets') + 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') + self.client.login(username=self.user.get_username(), password="password") response = self.client.post( - reverse('helpdesk:kb_vote', args=(self.kbitem1.pk, "up")), params={}) - cat_url = reverse('helpdesk:kb_category', - args=("test_cat",)) + "?kbitem=1" + reverse("helpdesk:kb_vote", args=(self.kbitem1.pk, "up")), params={} + ) + 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') + self.assertContains(response, "1 people found this answer useful of 1") response = self.client.post( - reverse('helpdesk:kb_vote', args=(self.kbitem1.pk, "down")), params={}) + reverse("helpdesk:kb_vote", args=(self.kbitem1.pk, "down")), params={} + ) self.assertRedirects(response, cat_url) response = self.client.get(cat_url) - self.assertContains(response, '0 people found this answer useful of 1') + 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") + response, + "'/tickets/submit/?queue=1&_readonly_fields_=queue&kbitem=1&submitter_email=foo%40bar.cz&title=lol", + ) diff --git a/helpdesk/tests/test_login.py b/helpdesk/tests/test_login.py index ad0ebddc..ef815d0a 100644 --- a/helpdesk/tests/test_login.py +++ b/helpdesk/tests/test_login.py @@ -3,43 +3,42 @@ from django.urls import reverse class TestLoginRedirect(TestCase): - - @override_settings(LOGIN_URL='/custom/login/') + @override_settings(LOGIN_URL="/custom/login/") def test_custom_login_view_with_url(self): """Test login redirect when LOGIN_URL is set to custom url""" - response = self.client.get(reverse('helpdesk:login')) + response = self.client.get(reverse("helpdesk:login")) # We expect that that helpdesk:home url is passed as next parameter in # the redirect url, so that the custom login can redirect the browser # back to helpdesk after the login. - home_url = reverse('helpdesk:home') - expected = '/custom/login/?next={}'.format(home_url) + home_url = reverse("helpdesk:home") + expected = "/custom/login/?next={}".format(home_url) self.assertRedirects(response, expected, fetch_redirect_response=False) - @override_settings(LOGIN_URL='/custom/login/') + @override_settings(LOGIN_URL="/custom/login/") def test_custom_login_next_param(self): """Test that the next url parameter is correctly relayed to custom login""" next_param = "/redirect/back" - url = reverse('helpdesk:login') + "?next=" + next_param + url = reverse("helpdesk:login") + "?next=" + next_param response = self.client.get(url) - expected = '/custom/login/?next={}'.format(next_param) + expected = "/custom/login/?next={}".format(next_param) self.assertRedirects(response, expected, fetch_redirect_response=False) - @override_settings(LOGIN_URL='helpdesk:login', SITE_ID=1) + @override_settings(LOGIN_URL="helpdesk:login", SITE_ID=1) def test_default_login_view(self): """Test that default login is used when LOGIN_URL is helpdesk:login""" - response = self.client.get(reverse('helpdesk:login')) - self.assertTemplateUsed(response, 'helpdesk/registration/login.html') + response = self.client.get(reverse("helpdesk:login")) + self.assertTemplateUsed(response, "helpdesk/registration/login.html") @override_settings(LOGIN_URL=None, SITE_ID=1) def test_login_url_none(self): """Test that default login is used when LOGIN_URL is None""" - response = self.client.get(reverse('helpdesk:login')) - self.assertTemplateUsed(response, 'helpdesk/registration/login.html') + response = self.client.get(reverse("helpdesk:login")) + self.assertTemplateUsed(response, "helpdesk/registration/login.html") - @override_settings(LOGIN_URL='admin:login', SITE_ID=1) + @override_settings(LOGIN_URL="admin:login", SITE_ID=1) def test_custom_login_view_with_name(self): """Test that LOGIN_URL can be a view name""" - response = self.client.get(reverse('helpdesk:login')) - home_url = reverse('helpdesk:home') - expected = reverse('admin:login') + "?next=" + home_url + response = self.client.get(reverse("helpdesk:login")) + home_url = reverse("helpdesk:home") + expected = reverse("admin:login") + "?next=" + home_url self.assertRedirects(response, expected) diff --git a/helpdesk/tests/test_markdown.py b/helpdesk/tests/test_markdown.py index 7eeccb02..e3a860ab 100644 --- a/helpdesk/tests/test_markdown.py +++ b/helpdesk/tests/test_markdown.py @@ -1,10 +1,10 @@ - from django.test import SimpleTestCase from helpdesk.models import get_markdown class MarkDown(SimpleTestCase): """Test work Markdown functional""" + def test_markdown_html_tab(self): expected_value = "

<div>test<div>

" input_value = "
test
" @@ -12,7 +12,7 @@ class MarkDown(SimpleTestCase): self.assertEqual(output_value, expected_value) def test_markdown_nl2br(self): - """ warning, after Line 1 - two withespace, esle did't work""" + """warning, after Line 1 - two withespace, esle did't work""" expected_value = "

Line 1
\n Line 2

" input_value = """Line 1 Line 2""" diff --git a/helpdesk/tests/test_navigation.py b/helpdesk/tests/test_navigation.py index b4a427ea..031e3fd7 100644 --- a/helpdesk/tests/test_navigation.py +++ b/helpdesk/tests/test_navigation.py @@ -27,15 +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.assertRaises(NoReverseMatch, reverse, 'helpdesk:kb_index') + 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')) + response = self.client.get(reverse("helpdesk:dashboard")) except NoReverseMatch as e: - if 'helpdesk:kb_index' in e.message: + if "helpdesk:kb_index" in e.message: self.fail( - "Please verify any unchecked references to helpdesk_kb_index (start with navigation.html)") + "Please verify any unchecked references to helpdesk_kb_index (start with navigation.html)" + ) else: raise else: @@ -47,7 +47,9 @@ class StaffUserTestCaseMixin(object): def setUp(self): self.original_setting = helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE - helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = self.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE + helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = ( + self.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE + ) self.reload_views() def tearDown(self): @@ -56,16 +58,16 @@ class StaffUserTestCaseMixin(object): def reload_views(self): try: - reload(sys.modules['helpdesk.decorators']) - reload(sys.modules['helpdesk.views.staff']) + reload(sys.modules["helpdesk.decorators"]) + reload(sys.modules["helpdesk.views.staff"]) reload_urlconf() except KeyError: pass def test_anonymous_user(self): """Access to the dashboard always requires a login""" - response = self.client.get(reverse('helpdesk:dashboard'), follow=True) - self.assertTemplateUsed(response, 'helpdesk/registration/login.html') + response = self.client.get(reverse("helpdesk:dashboard"), follow=True) + self.assertTemplateUsed(response, "helpdesk/registration/login.html") class NonStaffUsersAllowedTestCase(StaffUserTestCaseMixin, TestCase): @@ -79,13 +81,16 @@ 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') + username="henry.wensleydale", + password="gouda", + email="wensleydale@example.com", + ) self.assertTrue(is_helpdesk_staff(user)) - self.client.login(username=user.username, password='gouda') - response = self.client.get(reverse('helpdesk:dashboard'), follow=True) - self.assertTemplateUsed(response, 'helpdesk/dashboard.html') + self.client.login(username=user.username, password="gouda") + response = self.client.get(reverse("helpdesk:dashboard"), follow=True) + self.assertTemplateUsed(response, "helpdesk/dashboard.html") class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase): @@ -96,7 +101,10 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase): 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') + 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""" @@ -111,19 +119,18 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase): """ user = get_staff_user() - self.client.login(username=user.username, password='password') - response = self.client.get(reverse('helpdesk:dashboard'), follow=True) - self.assertTemplateUsed(response, 'helpdesk/dashboard.html') + self.client.login(username=user.username, password="password") + response = self.client.get(reverse("helpdesk:dashboard"), follow=True) + self.assertTemplateUsed(response, "helpdesk/dashboard.html") def test_non_staff_cannot_access_dashboard(self): """When HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False, non-staff users should not be able to access the dashboard. """ user = self.non_staff_user - 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') + 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") def test_staff_rss(self): """If HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False, @@ -131,9 +138,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) - self.assertContains(response, 'Unassigned Open and Reopened tickets') + 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) def test_non_staff_cannot_rss(self): @@ -141,31 +147,32 @@ 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_queue', args=['test_queue']), - reverse('helpdesk:rss_unassigned'), - reverse('helpdesk:rss_activity'), + reverse("helpdesk:rss_user", args=[user.username]), + 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): @staticmethod def custom_staff_filter(user): """Arbitrary user validation function""" - return user.is_authenticated and user.is_active and user.username.lower().endswith('wensleydale') + return ( + user.is_authenticated + and user.is_active + and user.username.lower().endswith("wensleydale") + ) HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = custom_staff_filter @@ -176,25 +183,29 @@ 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') + username="henry.wensleydale", + password="gouda", + email="wensleydale@example.com", + ) self.assertTrue(is_helpdesk_staff(user)) - self.client.login(username=user.username, password='gouda') - response = self.client.get(reverse('helpdesk:dashboard'), follow=True) - self.assertTemplateUsed(response, 'helpdesk/dashboard.html') + self.client.login(username=user.username, password="gouda") + response = self.client.get(reverse("helpdesk:dashboard"), follow=True) + self.assertTemplateUsed(response, "helpdesk/dashboard.html") 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') + username="terry.milton", password="frog", email="milton@example.com" + ) self.assertFalse(is_helpdesk_staff(user)) - self.client.login(username=user.username, password='frog') - response = self.client.get(reverse('helpdesk:dashboard'), follow=True) - self.assertTemplateUsed(response, 'helpdesk/registration/login.html') + self.client.login(username=user.username, password="frog") + response = self.client.get(reverse("helpdesk:dashboard"), follow=True) + self.assertTemplateUsed(response, "helpdesk/registration/login.html") class HomePageAnonymousUserTestCase(TestCase): @@ -206,14 +217,14 @@ class HomePageAnonymousUserTestCase(TestCase): def test_homepage(self): helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = True - response = self.client.get(reverse('helpdesk:home')) - self.assertTemplateUsed('helpdesk/public_homepage.html') + response = self.client.get(reverse("helpdesk:home")) + self.assertTemplateUsed("helpdesk/public_homepage.html") def test_redirect_to_login(self): """Unauthenticated users are redirected to the login page if HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT is True""" helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = True - response = self.client.get(reverse('helpdesk:home')) - self.assertRedirects(response, reverse('helpdesk:login')) + response = self.client.get(reverse("helpdesk:home")) + self.assertRedirects(response, reverse("helpdesk:login")) class HomePageTestCase(TestCase): @@ -221,17 +232,17 @@ class HomePageTestCase(TestCase): self.original_setting = helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = False try: - reload(sys.modules['helpdesk.views.public']) + reload(sys.modules["helpdesk.views.public"]) except KeyError: pass def tearDown(self): helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = self.original_setting - reload(sys.modules['helpdesk.views.public']) + reload(sys.modules["helpdesk.views.public"]) def assertUserRedirectedToView(self, user, view_name): - self.client.login(username=user.username, password='password') - response = self.client.get(reverse('helpdesk:home')) + self.client.login(username=user.username, password="password") + response = self.client.get(reverse("helpdesk:home")) self.assertRedirects(response, reverse(view_name)) self.client.logout() @@ -242,15 +253,16 @@ class HomePageTestCase(TestCase): # login_view_ticketlist is False... user.usersettings_helpdesk.login_view_ticketlist = False user.usersettings_helpdesk.save() - self.assertUserRedirectedToView(user, 'helpdesk:dashboard') + self.assertUserRedirectedToView(user, "helpdesk:dashboard") def test_no_user_settings_redirect_to_dashboard(self): """Authenticated users are redirected to the dashboard if user settings are missing""" from helpdesk.models import UserSettings + user = get_staff_user() UserSettings.objects.filter(user=user).delete() - self.assertUserRedirectedToView(user, 'helpdesk:dashboard') + self.assertUserRedirectedToView(user, "helpdesk:dashboard") def test_redirect_to_ticket_list(self): """Authenticated users are redirected to the ticket list based on their user settings""" @@ -258,7 +270,7 @@ class HomePageTestCase(TestCase): user.usersettings_helpdesk.login_view_ticketlist = True user.usersettings_helpdesk.save() - self.assertUserRedirectedToView(user, 'helpdesk:list') + self.assertUserRedirectedToView(user, "helpdesk:list") class ReturnToTicketTestCase(TestCase): @@ -268,13 +280,16 @@ class ReturnToTicketTestCase(TestCase): user = get_staff_user() ticket = create_ticket() response = return_to_ticket(user, helpdesk_settings, ticket) - self.assertEqual(response['location'], ticket.get_absolute_url()) + self.assertEqual(response["location"], ticket.get_absolute_url()) 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') + 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) + 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 b04f53bc..22cfe055 100644 --- a/helpdesk/tests/test_per_queue_staff_permission.py +++ b/helpdesk/tests/test_per_queue_staff_permission.py @@ -10,7 +10,6 @@ from helpdesk.user import HelpdeskUser class PerQueueStaffMembershipTestCase(TestCase): - IDENTIFIERS = (1, 2) def setUp(self): @@ -19,31 +18,33 @@ class PerQueueStaffMembershipTestCase(TestCase): and user_2 with access to queue_2 containing 4 tickets and superuser who should be able to access both queues """ - self.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION + self.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = ( + settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION + ) settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = True self.client = Client() User = get_user_model() self.superuser = User.objects.create( - username='superuser', + username="superuser", is_staff=True, is_superuser=True, ) - self.superuser.set_password('superuser') + self.superuser.set_password("superuser") self.superuser.save() self.identifier_users = {} for identifier in self.IDENTIFIERS: - queue = self.__dict__['queue_%d' % identifier] = Queue.objects.create( - title='Queue %d' % identifier, - slug='q%d' % identifier, + queue = self.__dict__["queue_%d" % identifier] = Queue.objects.create( + title="Queue %d" % identifier, + slug="q%d" % identifier, ) - user = self.__dict__['user_%d' % identifier] = User.objects.create( - username='User_%d' % identifier, + user = self.__dict__["user_%d" % identifier] = User.objects.create( + username="User_%d" % identifier, is_staff=True, - email="foo%s@example.com" % identifier + email="foo%s@example.com" % identifier, ) user.set_password(str(identifier)) user.save() @@ -55,13 +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, ) @@ -70,7 +71,9 @@ class PerQueueStaffMembershipTestCase(TestCase): """ Reset HELPDESK_ENABLE_PER_QUEUE_STAFF_MEMBERSHIP to original value """ - settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = self.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION + settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = ( + self.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION + ) def test_dashboard_ticket_counts(self): """ @@ -81,33 +84,32 @@ class PerQueueStaffMembershipTestCase(TestCase): # Regular users for identifier in self.IDENTIFIERS: - self.client.login(username='User_%d' % - identifier, password=str(identifier)) - response = self.client.get(reverse('helpdesk:dashboard')) + self.client.login(username="User_%d" % identifier, password=str(identifier)) + response = self.client.get(reverse("helpdesk:dashboard")) self.assertEqual( - len(response.context['unassigned_tickets']), + len(response.context["unassigned_tickets"]), identifier, - 'Unassigned tickets were not properly limited by queue membership' + "Unassigned tickets were not properly limited by queue membership", ) self.assertEqual( - response.context['basic_ticket_stats']['open_ticket_stats'][0][1], + response.context["basic_ticket_stats"]["open_ticket_stats"][0][1], identifier * 2, - 'Basic ticket stats were not properly limited by queue membership' + "Basic ticket stats were not properly limited by queue membership", ) # Superuser - self.client.login(username='superuser', password='superuser') - response = self.client.get(reverse('helpdesk:dashboard')) + self.client.login(username="superuser", password="superuser") + response = self.client.get(reverse("helpdesk:dashboard")) self.assertEqual( - len(response.context['unassigned_tickets']), + len(response.context["unassigned_tickets"]), 3, - 'Unassigned tickets were limited by queue membership for a superuser' + "Unassigned tickets were limited by queue membership for a superuser", ) self.assertEqual( - response.context['basic_ticket_stats']['open_ticket_stats'][0][1] + - response.context['basic_ticket_stats']['open_ticket_stats'][1][1], + response.context["basic_ticket_stats"]["open_ticket_stats"][0][1] + + response.context["basic_ticket_stats"]["open_ticket_stats"][1][1], 6, - 'Basic ticket stats were limited by queue membership for a superuser' + "Basic ticket stats were limited by queue membership for a superuser", ) def test_report_ticket_counts(self): @@ -119,44 +121,43 @@ class PerQueueStaffMembershipTestCase(TestCase): # Regular users for identifier in self.IDENTIFIERS: - self.client.login(username='User_%d' % - identifier, password=str(identifier)) - response = self.client.get(reverse('helpdesk:report_index')) + 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']), + len(response.context["dash_tickets"]), 1, - 'The queues in dash_tickets were not properly limited by queue membership' + "The queues in dash_tickets were not properly limited by queue membership", ) self.assertEqual( - response.context['dash_tickets'][0]['open'], + response.context["dash_tickets"][0]["open"], identifier * 2, - 'The tickets in dash_tickets were not properly limited by queue membership' + "The tickets in dash_tickets were not properly limited by queue membership", ) self.assertEqual( - response.context['basic_ticket_stats']['open_ticket_stats'][0][1], + response.context["basic_ticket_stats"]["open_ticket_stats"][0][1], identifier * 2, - 'Basic ticket stats were not properly limited by queue membership' + "Basic ticket stats were not properly limited by queue membership", ) # Superuser - self.client.login(username='superuser', password='superuser') - response = self.client.get(reverse('helpdesk:report_index')) + self.client.login(username="superuser", password="superuser") + response = self.client.get(reverse("helpdesk:report_index")) self.assertEqual( - len(response.context['dash_tickets']), + len(response.context["dash_tickets"]), 2, - 'The queues in dash_tickets were limited by queue membership for a superuser' + "The queues in dash_tickets were limited by queue membership for a superuser", ) self.assertEqual( - response.context['dash_tickets'][0]['open'] + - response.context['dash_tickets'][1]['open'], + response.context["dash_tickets"][0]["open"] + + response.context["dash_tickets"][1]["open"], 6, - 'The tickets in dash_tickets were limited by queue membership for a superuser' + "The tickets in dash_tickets were limited by queue membership for a superuser", ) self.assertEqual( - response.context['basic_ticket_stats']['open_ticket_stats'][0][1] + - response.context['basic_ticket_stats']['open_ticket_stats'][1][1], + response.context["basic_ticket_stats"]["open_ticket_stats"][0][1] + + response.context["basic_ticket_stats"]["open_ticket_stats"][1][1], 6, - 'Basic ticket stats were limited by queue membership for a superuser' + "Basic ticket stats were limited by queue membership for a superuser", ) def test_ticket_list_per_queue_user_restrictions(self): @@ -167,36 +168,38 @@ class PerQueueStaffMembershipTestCase(TestCase): """ # Regular users for identifier in self.IDENTIFIERS: - 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() + 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() self.assertEqual( len(tickets), identifier * 2, - 'Ticket list was not properly limited by queue membership' + "Ticket list was not properly limited by queue membership", ) self.assertEqual( - len(response.context['queue_choices']), + len(response.context["queue_choices"]), 1, - 'Queue choices were not properly limited by queue membership' + "Queue choices were not properly limited by queue membership", ) self.assertEqual( - response.context['queue_choices'][0], + response.context["queue_choices"][0], Queue.objects.get(title="Queue %d" % identifier), - 'Queue choices were not properly limited by queue membership' + "Queue choices were not properly limited by queue membership", ) # 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() + 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() self.assertEqual( len(tickets), 6, - 'Ticket list was limited by queue membership for a superuser' + "Ticket list was limited by queue membership for a superuser", ) def test_ticket_reports_per_queue_user_restrictions(self): @@ -207,61 +210,60 @@ 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'}) + reverse("helpdesk:run_report", kwargs={"report": "userqueue"}) ) # Only two columns of data should be present: ticket counts for # unassigned and this user only self.assertEqual( - len(response.context['data']), + len(response.context["data"]), 2, - 'Queues in report were not properly limited by queue membership' + "Queues in report were not properly limited by queue membership", ) # 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' + "Tickets in report were not properly limited by queue membership", ) # Each user should only be able to pick 1 queue self.assertEqual( - len(response.context['headings']), + len(response.context["headings"]), 2, - 'Queue choices were not properly limited by queue membership' + "Queue choices were not properly limited by queue membership", ) # The queue each user can pick should be the queue named after # their ID self.assertEqual( - response.context['headings'][1], + response.context["headings"][1], "Queue %d" % identifier, - 'Queue choices were not properly limited by queue membership' + "Queue choices were not properly limited by queue membership", ) # Superuser - self.client.login(username='superuser', password='superuser') + self.client.login(username="superuser", password="superuser") response = self.client.get( - reverse('helpdesk:run_report', kwargs={'report': 'userqueue'}) + reverse("helpdesk:run_report", kwargs={"report": "userqueue"}) ) # Superuser should see ticket counts for all two queues, which includes # three columns: unassigned and both user 1 and user 2 self.assertEqual( - len(response.context['data'][0]), + len(response.context["data"][0]), 3, - 'Queues in report were improperly limited by queue membership for a superuser' + "Queues in report were improperly limited by queue membership for a superuser", ) # 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' + "Tickets in report were improperly limited by queue membership for a superuser", ) self.assertEqual( - len(response.context['headings']), + len(response.context["headings"]), 3, - 'Queue choices were improperly limited by queue membership for a superuser' + "Queue choices 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 f07c2fdf..edac0da6 100644 --- a/helpdesk/tests/test_public_actions.py +++ b/helpdesk/tests/test_public_actions.py @@ -16,39 +16,51 @@ class PublicActionsTestCase(TestCase): """ Create a queue & ticket we can use for later tests. """ - self.queue = Queue.objects.create(title='Queue 1', - slug='q', - allow_public_submission=True, - new_ticket_cc='new.public@example.com', - updated_ticket_cc='update.public@example.com') - self.ticket = Ticket.objects.create(title='Test Ticket', - queue=self.queue, - submitter_email='test.submitter@example.com', - description='This is a test ticket.') + self.queue = Queue.objects.create( + title="Queue 1", + slug="q", + allow_public_submission=True, + new_ticket_cc="new.public@example.com", + updated_ticket_cc="update.public@example.com", + ) + self.ticket = Ticket.objects.create( + title="Test Ticket", + queue=self.queue, + submitter_email="test.submitter@example.com", + description="This is a test ticket.", + ) self.client = Client() def test_public_view_ticket(self): # Without key, we get 403 - response = self.client.get('%s?ticket=%s&email=%s' % ( - reverse('helpdesk:public_view'), - self.ticket.ticket_for_url, - 'test.submitter@example.com')) + response = self.client.get( + "%s?ticket=%s&email=%s" + % ( + reverse("helpdesk:public_view"), + self.ticket.ticket_for_url, + "test.submitter@example.com", + ) + ) self.assertEqual(response.status_code, 403) - self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html') + self.assertTemplateNotUsed(response, "helpdesk/public_view_form.html") # With a key it works - response = self.client.get('%s?ticket=%s&email=%s&key=%s' % ( - reverse('helpdesk:public_view'), - self.ticket.ticket_for_url, - 'test.submitter@example.com', - self.ticket.secret_key)) + response = self.client.get( + "%s?ticket=%s&email=%s&key=%s" + % ( + reverse("helpdesk:public_view"), + self.ticket.ticket_for_url, + "test.submitter@example.com", + self.ticket.secret_key, + ) + ) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'helpdesk/public_view_ticket.html') + self.assertTemplateUsed(response, "helpdesk/public_view_ticket.html") def test_public_close(self): old_status = self.ticket.status old_resolution = self.ticket.resolution - resolution_text = 'Resolved by test script' + resolution_text = "Resolved by test script" ticket = Ticket.objects.get(id=self.ticket.id) @@ -58,20 +70,23 @@ class PublicActionsTestCase(TestCase): current_followups = ticket.followup_set.all().count() - response = self.client.get('%s?ticket=%s&email=%s&close&key=%s' % ( - reverse('helpdesk:public_view'), - ticket.ticket_for_url, - 'test.submitter@example.com', - ticket.secret_key)) + response = self.client.get( + "%s?ticket=%s&email=%s&close&key=%s" + % ( + reverse("helpdesk:public_view"), + ticket.ticket_for_url, + "test.submitter@example.com", + ticket.secret_key, + ) + ) ticket = Ticket.objects.get(id=self.ticket.id) self.assertEqual(response.status_code, 302) - self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html') + 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 f13ce1cd..3e7bf3a5 100644 --- a/helpdesk/tests/test_query.py +++ b/helpdesk/tests/test_query.py @@ -47,25 +47,57 @@ class QueryTests(TestCase): """Create a staff user and login""" User = get_user_model() self.user = User.objects.create( - username='User_1', + username="User_1", is_staff=is_staff, ) - self.user.set_password('pass') + self.user.set_password("pass") self.user.save() - self.client.login(username='User_1', password='pass') + self.client.login(username="User_1", password="pass") def test_query_basic(self): self.loginUser() query = query_to_base64({}) response = self.client.get( - reverse('helpdesk:datatables_ticket_list', args=[query])) + reverse("helpdesk:datatables_ticket_list", args=[query]) + ) resp_json = response.json() self.assertEqual( resp_json, { - "data": - [{"ticket": "1 [test_queue-1]", "id": 1, "priority": 3, "title": "unassigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": ""}, - {"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": resp_json["data"][1]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}], + "data": [ + { + "ticket": "1 [test_queue-1]", + "id": 1, + "priority": 3, + "title": "unassigned to kbitem", + "queue": {"title": "Test queue", "id": 1}, + "status": "Open", + "created": resp_json["data"][0]["created"], + "due_date": None, + "assigned_to": "None", + "submitter": None, + "last_followup": None, + "row_class": "", + "time_spent": "", + "kbitem": "", + }, + { + "ticket": "2 [test_queue-2]", + "id": 2, + "priority": 3, + "title": "assigned to kbitem", + "queue": {"title": "Test queue", "id": 1}, + "status": "Open", + "created": resp_json["data"][1]["created"], + "due_date": None, + "assigned_to": "None", + "submitter": None, + "last_followup": None, + "row_class": "", + "time_spent": "", + "kbitem": "KBItem 1", + }, + ], "recordsFiltered": 2, "recordsTotal": 2, "draw": 0, @@ -74,18 +106,32 @@ class QueryTests(TestCase): def test_query_by_kbitem(self): self.loginUser() - query = query_to_base64( - {'filtering': {'kbitem__in': [self.kbitem1.pk]}} - ) + query = query_to_base64({"filtering": {"kbitem__in": [self.kbitem1.pk]}}) response = self.client.get( - reverse('helpdesk:datatables_ticket_list', args=[query])) + reverse("helpdesk:datatables_ticket_list", args=[query]) + ) resp_json = response.json() self.assertEqual( resp_json, { - "data": - [{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", - "created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}], + "data": [ + { + "ticket": "2 [test_queue-2]", + "id": 2, + "priority": 3, + "title": "assigned to kbitem", + "queue": {"title": "Test queue", "id": 1}, + "status": "Open", + "created": resp_json["data"][0]["created"], + "due_date": None, + "assigned_to": "None", + "submitter": None, + "last_followup": None, + "row_class": "", + "time_spent": "", + "kbitem": "KBItem 1", + } + ], "recordsFiltered": 1, "recordsTotal": 1, "draw": 0, @@ -94,18 +140,32 @@ class QueryTests(TestCase): def test_query_by_no_kbitem(self): self.loginUser() - query = query_to_base64( - {'filtering_null': {'kbitem__isnull': True}} - ) + query = query_to_base64({"filtering_null": {"kbitem__isnull": True}}) response = self.client.get( - reverse('helpdesk:datatables_ticket_list', args=[query])) + reverse("helpdesk:datatables_ticket_list", args=[query]) + ) resp_json = response.json() self.assertEqual( resp_json, { - "data": - [{"ticket": "1 [test_queue-1]", "id": 1, "priority": 3, "title": "unassigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", - "created": resp_json["data"][0]["created"], "due_date": None, "assigned_to": "None", "submitter": None, "last_followup": None, "row_class": "", "time_spent": "", "kbitem": ""}], + "data": [ + { + "ticket": "1 [test_queue-1]", + "id": 1, + "priority": 3, + "title": "unassigned to kbitem", + "queue": {"title": "Test queue", "id": 1}, + "status": "Open", + "created": resp_json["data"][0]["created"], + "due_date": None, + "assigned_to": "None", + "submitter": None, + "last_followup": None, + "row_class": "", + "time_spent": "", + "kbitem": "", + } + ], "recordsFiltered": 1, "recordsTotal": 1, "draw": 0, diff --git a/helpdesk/tests/test_savequery.py b/helpdesk/tests/test_savequery.py index b7130d25..77abcc3b 100644 --- a/helpdesk/tests/test_savequery.py +++ b/helpdesk/tests/test_savequery.py @@ -7,24 +7,25 @@ from helpdesk.tests.helpers import get_user class TestSavingSharedQuery(TestCase): def setUp(self): - q = Queue(title='Q1', slug='q1') + q = Queue(title="Q1", slug="q1") q.save() self.q = q def test_cansavequery(self): """Can a query be saved""" - url = reverse('helpdesk:savequery') - self.client.login(username=get_user(is_staff=True).get_username(), - password='password') + url = reverse("helpdesk:savequery") + self.client.login( + username=get_user(is_staff=True).get_username(), password="password" + ) response = self.client.post( url, data={ - 'title': 'ticket on my queue', - 'queue': self.q, - 'shared': 'on', - 'query_encoded': - 'KGRwMApWZmlsdGVyaW5nCnAxCihkcDIKVnN0YXR1c19faW4KcDMKKG' - 'xwNApJMQphSTIKYUkzCmFzc1Zzb3J0aW5nCnA1ClZjcmVhdGVkCnA2CnMu' - }) + "title": "ticket on my queue", + "queue": self.q, + "shared": "on", + "query_encoded": "KGRwMApWZmlsdGVyaW5nCnAxCihkcDIKVnN0YXR1c19faW4KcDMKKG" + "xwNApJMQphSTIKYUkzCmFzc1Zzb3J0aW5nCnA1ClZjcmVhdGVkCnA2CnMu", + }, + ) self.assertEqual(response.status_code, 302) - self.assertTrue('tickets/?saved_query=1' in response.url) + self.assertTrue("tickets/?saved_query=1" in response.url) diff --git a/helpdesk/tests/test_ticket_actions.py b/helpdesk/tests/test_ticket_actions.py index 0daf1f1d..19b6fa8d 100644 --- a/helpdesk/tests/test_ticket_actions.py +++ b/helpdesk/tests/test_ticket_actions.py @@ -17,29 +17,29 @@ except ImportError: # python 2 class TicketActionsTestCase(TestCase): - fixtures = ['emailtemplate.json'] + fixtures = ["emailtemplate.json"] def setUp(self): self.queue_public = Queue.objects.create( - title='Queue 1', - slug='q1', + title="Queue 1", + slug="q1", allow_public_submission=True, - new_ticket_cc='new.public@example.com', - updated_ticket_cc='update.public@example.com' + new_ticket_cc="new.public@example.com", + updated_ticket_cc="update.public@example.com", ) self.queue_private = Queue.objects.create( - title='Queue 2', - slug='q2', + title="Queue 2", + slug="q2", allow_public_submission=False, - new_ticket_cc='new.private@example.com', - updated_ticket_cc='update.private@example.com' + new_ticket_cc="new.private@example.com", + updated_ticket_cc="update.private@example.com", ) self.ticket_data = { - 'queue': self.queue_public, - 'title': 'Test Ticket', - 'description': 'Some Test Ticket', + "queue": self.queue_public, + "title": "Test Ticket", + "description": "Some Test Ticket", } self.client = Client() @@ -49,24 +49,22 @@ class TicketActionsTestCase(TestCase): """Create a staff user and login""" User = get_user_model() self.user = User.objects.create( - username='User_1', + username="User_1", is_staff=is_staff, ) - self.user.set_password('pass') + self.user.set_password("pass") self.user.save() - self.client.login(username='User_1', password='pass') + self.client.login(username="User_1", password="pass") def test_ticket_markdown(self): - ticket_data = { - 'queue': self.queue_public, - 'title': 'Test Ticket', - 'description': '*bold*', + "queue": self.queue_public, + "title": "Test Ticket", + "description": "*bold*", } ticket = Ticket.objects.create(**ticket_data) - self.assertEqual(ticket.get_markdown(), - "

bold

") + self.assertEqual(ticket.get_markdown(), "

bold

") def test_delete_ticket_staff(self): # make staff user @@ -76,13 +74,14 @@ class TicketActionsTestCase(TestCase): ticket = Ticket.objects.create(**self.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] @@ -90,7 +89,7 @@ class TicketActionsTestCase(TestCase): # Django 1.9 compatible way of testing this # https://docs.djangoproject.com/en/1.9/releases/1.9/#http-redirects-no-longer-forced-to-absolute-uris urlparts = urlparse(first_redirect_url) - self.assertEqual(urlparts.path, reverse('helpdesk:home')) + self.assertEqual(urlparts.path, reverse("helpdesk:home")) # test ticket deleted with self.assertRaises(Ticket.DoesNotExist): @@ -105,15 +104,15 @@ class TicketActionsTestCase(TestCase): # create second user User = get_user_model() self.user2 = User.objects.create( - username='User_2', + username="User_2", is_staff=True, ) initial_data = { - 'title': 'Private ticket test', - 'queue': self.queue_public, - 'assigned_to': self.user, - 'status': Ticket.OPEN_STATUS, + "title": "Private ticket test", + "queue": self.queue_public, + "assigned_to": self.user, + "status": Ticket.OPEN_STATUS, } # create ticket @@ -122,39 +121,45 @@ class TicketActionsTestCase(TestCase): # assign new owner post_data = { - 'owner': self.user2.id, + "owner": self.user2.id, } - 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') + 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, # which triggers emails being sent ticket.assigned_to = self.user2 - ticket.submitter_email = 'submitter@test.com' + ticket.submitter_email = "submitter@test.com" ticket.save() - self.user2.email = 'user2@test.com' + self.user2.email = "user2@test.com" self.user2.save() - self.user.email = 'user1@test.com' + self.user.email = "user1@test.com" self.user.save() - post_data = { - 'new_status': Ticket.CLOSED_STATUS, - 'public': True - } + post_data = {"new_status": Ticket.CLOSED_STATUS, "public": True} # 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) - self.assertContains(response, 'Changed Status from Open to Closed') + 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 + "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) - self.assertContains(response, 'Changed Status from Open to Closed') + 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): """Tests whether non-staff but assigned user still counts as owner""" @@ -165,24 +170,22 @@ class TicketActionsTestCase(TestCase): # create second user User = get_user_model() self.user2 = User.objects.create( - username='User_2', + username="User_2", is_staff=False, ) initial_data = { - 'title': 'Private ticket test', - 'queue': self.queue_private, - 'assigned_to': self.user, - 'status': Ticket.OPEN_STATUS, + "title": "Private ticket test", + "queue": self.queue_private, + "assigned_to": self.user, + "status": Ticket.OPEN_STATUS, } # 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""" @@ -191,10 +194,10 @@ class TicketActionsTestCase(TestCase): self.loginUser() initial_data = { - 'title': 'Some private ticket', - 'queue': self.queue_public, - 'assigned_to': self.user, - 'status': Ticket.OPEN_STATUS, + "title": "Some private ticket", + "queue": self.queue_public, + "assigned_to": self.user, + "status": Ticket.OPEN_STATUS, } # create ticket @@ -202,18 +205,23 @@ class TicketActionsTestCase(TestCase): ticket_id = ticket.id # generate the URL text - result = num_to_link('this is ticket#%s' % ticket_id) + result = num_to_link("this is ticket#%s" % ticket_id) self.assertEqual( - result, "this is ticket #%s" % (ticket_id, ticket_id)) + result, + "this is ticket #%s" + % (ticket_id, ticket_id), + ) - result2 = num_to_link( - 'whoa another ticket is here #%s huh' % 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, + "whoa another ticket is here #%s huh" + % (ticket_id, ticket_id), + ) def test_create_ticket_getform(self): self.loginUser() - response = self.client.get(reverse('helpdesk:submit'), follow=True) + response = self.client.get(reverse("helpdesk:submit"), follow=True) self.assertEqual(response.status_code, 200) # TODO this needs to be checked further @@ -224,61 +232,62 @@ class TicketActionsTestCase(TestCase): # Create two tickets ticket_1 = Ticket.objects.create( queue=self.queue_public, - title='Ticket 1', - description='Description from ticket 1', - submitter_email='user1@mail.com', + title="Ticket 1", + description="Description from ticket 1", + submitter_email="user1@mail.com", status=Ticket.RESOLVED_STATUS, - resolution='Awesome resolution for ticket 1' + 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() ticket_2 = Ticket.objects.create( queue=self.queue_public, - title='Ticket 2', - description='Description from ticket 2', - submitter_email='user2@mail.com', + title="Ticket 2", + description="Description from ticket 2", + submitter_email="user2@mail.com", due_date=due_date, - assigned_to=self.user + assigned_to=self.user, ) - 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') + 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 custom_field_1 = CustomField.objects.create( - name='test', - label='Test', - data_type='varchar', + name="test", + label="Test", + data_type="varchar", ) - ticket_1_field_1 = 'This is for the test field' + 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_2_field_1 = 'Another test text' - 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', + field=custom_field_1, value=ticket_1_field_1 ) - ticket_2_field_2 = '444' + ticket_2_field_1 = "Another test text" ticket_2.ticketcustomfieldvalue_set.create( - field=custom_field_2, value=ticket_2_field_2) + 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 + ) # Check that it correctly redirects to the intermediate page response = self.client.post( - reverse('helpdesk:mass_update'), - data={ - 'ticket_id': [str(ticket_1.id), str(ticket_2.id)], - 'action': 'merge' - }, - follow=True + reverse("helpdesk:mass_update"), + data={"ticket_id": [str(ticket_1.id), str(ticket_2.id)], "action": "merge"}, + 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) @@ -293,16 +302,16 @@ class TicketActionsTestCase(TestCase): response = self.client.post( redirect_url, data={ - 'chosen_ticket': str(ticket_1.id), - 'due_date': str(ticket_2.id), - 'status': str(ticket_1.id), - 'submitter_email': str(ticket_2.id), - 'description': str(ticket_2.id), - 'assigned_to': str(ticket_2.id), + "chosen_ticket": str(ticket_1.id), + "due_date": str(ticket_2.id), + "status": str(ticket_1.id), + "submitter_email": str(ticket_2.id), + "description": str(ticket_2.id), + "assigned_to": str(ticket_2.id), custom_field_1.name: str(ticket_1.id), custom_field_2.name: str(ticket_2.id), }, - follow=True + follow=True, ) self.assertRedirects(response, ticket_1.get_absolute_url()) ticket_2.refresh_from_db() @@ -316,14 +325,18 @@ 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]) def test_update_ticket_queue(self): """Tests whether user can change the queue in the Respond to this ticket section.""" @@ -333,10 +346,10 @@ class TicketActionsTestCase(TestCase): # create ticket initial_data = { - 'title': 'Queue change ticket test', - 'queue': self.queue_public, - 'assigned_to': self.user, - 'status': Ticket.OPEN_STATUS, + "title": "Queue change ticket test", + "queue": self.queue_public, + "assigned_to": self.user, + "status": Ticket.OPEN_STATUS, } ticket = Ticket.objects.create(**initial_data) ticket_id = ticket.id @@ -346,24 +359,24 @@ class TicketActionsTestCase(TestCase): # POST first follow-up with new queue new_queue = Queue.objects.create( - title='New Queue', - slug='newqueue', + title="New Queue", + slug="newqueue", ) post_data = { - 'comment': 'first follow-up in new queue', - 'queue': str(new_queue.id), + "comment": "first follow-up in new queue", + "queue": str(new_queue.id), } - response = self.client.post(reverse('helpdesk:update', - kwargs={'ticket_id': ticket_id}), - post_data) + response = self.client.post( + reverse("helpdesk:update", kwargs={"ticket_id": ticket_id}), post_data + ) # queue was correctly modified ticket.refresh_from_db() self.assertEqual(ticket.queue, new_queue) # ticket change was saved - latest_fup = ticket.followup_set.latest('date') - latest_ticketchange = latest_fup.ticketchange_set.latest('id') - self.assertEqual(latest_ticketchange.field, _('Queue')) + latest_fup = ticket.followup_set.latest("date") + latest_ticketchange = latest_fup.ticketchange_set.latest("id") + self.assertEqual(latest_ticketchange.field, _("Queue")) self.assertEqual(int(latest_ticketchange.old_value), self.queue_public.id) - self.assertEqual(int(latest_ticketchange.new_value), new_queue.id) \ No newline at end of file + self.assertEqual(int(latest_ticketchange.new_value), new_queue.id) diff --git a/helpdesk/tests/test_ticket_lookup.py b/helpdesk/tests/test_ticket_lookup.py index 8b3fcaba..374b0c8f 100644 --- a/helpdesk/tests/test_ticket_lookup.py +++ b/helpdesk/tests/test_ticket_lookup.py @@ -9,14 +9,12 @@ from helpdesk.models import Queue, Ticket User = get_user_model() -@override_settings( - HELPDESK_VIEW_A_TICKET_PUBLIC=True -) +@override_settings(HELPDESK_VIEW_A_TICKET_PUBLIC=True) class TestTicketLookupPublicEnabled(TestCase): def setUp(self): - q = Queue(title='Q1', slug='q1') + q = Queue(title="Q1", slug="q1") q.save() - t = Ticket(title='Test Ticket', submitter_email='test@domain.com') + t = Ticket(title="Test Ticket", submitter_email="test@domain.com") t.queue = q t.save() self.ticket = t @@ -33,20 +31,26 @@ class TestTicketLookupPublicEnabled(TestCase): # we will exercise 'reverse' to lookup/build the URL # from the ticket info we have # http://example.com/helpdesk/view/?ticket=q1-1&email=None - response = self.client.get(reverse('helpdesk:public_view'), - {'ticket': self.ticket.ticket_for_url, - 'email': self.ticket.submitter_email}) + response = self.client.get( + reverse("helpdesk:public_view"), + { + "ticket": self.ticket.ticket_for_url, + "email": self.ticket.submitter_email, + }, + ) self.assertEqual(response.status_code, 200) def test_ticket_with_changed_queue(self): # Make a ticket (already done in setup() ) # Now make another queue - q2 = Queue(title='Q2', slug='q2') + q2 = Queue(title="Q2", slug="q2") q2.save() # grab the URL / params which would have been emailed out to submitter. - url = reverse('helpdesk:public_view') - params = {'ticket': self.ticket.ticket_for_url, - 'email': self.ticket.submitter_email} + url = reverse("helpdesk:public_view") + params = { + "ticket": self.ticket.ticket_for_url, + "email": self.ticket.submitter_email, + } # Pickup the ticket created in setup() and change its queue self.ticket.queue = q2 self.ticket.save() @@ -56,36 +60,34 @@ class TestTicketLookupPublicEnabled(TestCase): self.assertNotContains(response, "Invalid ticket ID") def test_add_email_to_ticketcc_if_not_in(self): - staff_email = 'staff@mail.com' + staff_email = "staff@mail.com" staff_user = User.objects.create( - username='staff', email=staff_email, is_staff=True) + username="staff", email=staff_email, is_staff=True + ) self.ticket.assigned_to = staff_user self.ticket.save() - email_1 = 'user1@mail.com' + email_1 = "user1@mail.com" ticketcc_1 = self.ticket.ticketcc_set.create(email=email_1) # Add new email to CC - email_2 = 'user2@mail.com' + 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) + 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 06d6fe0d..b644c08c 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -1,5 +1,3 @@ - - from django.contrib.auth import get_user_model from django.core import mail from django.test import TestCase @@ -7,39 +5,49 @@ from django.test.client import Client from django.urls import reverse import email from helpdesk.email import extract_email_metadata -from helpdesk.models import CustomField, FollowUp, KBCategory, KBItem, Queue, Ticket, TicketCC +from helpdesk.models import ( + CustomField, + FollowUp, + KBCategory, + KBItem, + Queue, + Ticket, + TicketCC, +) import logging from urllib.parse import urlparse import uuid -logger = logging.getLogger('helpdesk') +logger = logging.getLogger("helpdesk") class TicketBasicsTestCase(TestCase): - fixtures = ['emailtemplate.json'] + fixtures = ["emailtemplate.json"] def setUp(self): self.queue_public = Queue.objects.create( - title='Queue 1', - slug='q1', + title="Queue 1", + slug="q1", allow_public_submission=True, - new_ticket_cc='new.public@example.com', - updated_ticket_cc='update.public@example.com') + new_ticket_cc="new.public@example.com", + updated_ticket_cc="update.public@example.com", + ) self.queue_private = Queue.objects.create( - title='Queue 2', - slug='q2', + title="Queue 2", + slug="q2", allow_public_submission=False, - new_ticket_cc='new.private@example.com', - updated_ticket_cc='update.private@example.com') + new_ticket_cc="new.private@example.com", + updated_ticket_cc="update.private@example.com", + ) self.ticket_data = { - 'title': 'Test Ticket', - 'description': 'Some Test Ticket', + "title": "Test Ticket", + "description": "Some Test Ticket", } self.user = get_user_model().objects.create( - username='User_1', + username="User_1", ) self.client = Client() @@ -58,19 +66,18 @@ class TicketBasicsTestCase(TestCase): def test_create_ticket_public(self): email_count = len(mail.outbox) - response = self.client.get(reverse('helpdesk:home')) + response = self.client.get(reverse("helpdesk:home")) self.assertEqual(response.status_code, 200) post_data = { - 'title': 'Test ticket title', - 'queue': self.queue_public.id, - 'submitter_email': 'ticket1.submitter@example.com', - 'body': 'Test ticket body', - 'priority': 3, + "title": "Test ticket title", + "queue": self.queue_public.id, + "submitter_email": "ticket1.submitter@example.com", + "body": "Test ticket body", + "priority": 3, } - response = self.client.post( - reverse('helpdesk:home'), post_data, follow=True) + 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] @@ -79,7 +86,7 @@ class TicketBasicsTestCase(TestCase): # Django 1.9 compatible way of testing this # https://docs.djangoproject.com/en/1.9/releases/1.9/#http-redirects-no-longer-forced-to-absolute-uris urlparts = urlparse(last_redirect_url) - self.assertEqual(urlparts.path, reverse('helpdesk:public_view')) + self.assertEqual(urlparts.path, reverse("helpdesk:public_view")) # Ensure submitter, new-queue + update-queue were all emailed. self.assertEqual(email_count + 3, len(mail.outbox)) @@ -92,19 +99,20 @@ class TicketBasicsTestCase(TestCase): def test_create_ticket_public_with_hidden_fields(self): email_count = len(mail.outbox) - response = self.client.get(reverse('helpdesk:home')) + response = self.client.get(reverse("helpdesk:home")) self.assertEqual(response.status_code, 200) post_data = { - 'title': 'Test ticket title', - 'queue': self.queue_public.id, - 'submitter_email': 'ticket1.submitter@example.com', - 'body': 'Test ticket body', - 'priority': 4, + "title": "Test ticket title", + "queue": self.queue_public.id, + "submitter_email": "ticket1.submitter@example.com", + "body": "Test ticket body", + "priority": 4, } response = self.client.post( - reverse('helpdesk:home') + "?_hide_fields_=priority", post_data, follow=True) + reverse("helpdesk:home") + "?_hide_fields_=priority", post_data, follow=True + ) ticket = Ticket.objects.last() self.assertEqual(ticket.priority, 4) @@ -112,19 +120,18 @@ class TicketBasicsTestCase(TestCase): email_count = len(mail.outbox) self.client.force_login(self.user) - response = self.client.get(reverse('helpdesk:home')) + response = self.client.get(reverse("helpdesk:home")) self.assertEqual(response.status_code, 200) post_data = { - 'title': 'Test ticket title', - 'queue': self.queue_public.id, - 'submitter_email': 'ticket1.submitter@example.com', - 'body': 'Test ticket body', - 'priority': 3, + "title": "Test ticket title", + "queue": self.queue_public.id, + "submitter_email": "ticket1.submitter@example.com", + "body": "Test ticket body", + "priority": 3, } - response = self.client.post( - reverse('helpdesk:home'), post_data, follow=True) + 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] @@ -133,7 +140,7 @@ class TicketBasicsTestCase(TestCase): # Django 1.9 compatible way of testing this # https://docs.djangoproject.com/en/1.9/releases/1.9/#http-redirects-no-longer-forced-to-absolute-uris urlparts = urlparse(last_redirect_url) - self.assertEqual(urlparts.path, reverse('helpdesk:public_view')) + self.assertEqual(urlparts.path, reverse("helpdesk:public_view")) # Ensure submitter, new-queue + update-queue were all emailed. self.assertEqual(email_count + 3, len(mail.outbox)) @@ -146,44 +153,45 @@ class TicketBasicsTestCase(TestCase): def test_create_ticket_private(self): email_count = len(mail.outbox) post_data = { - 'title': 'Private ticket test', - 'queue': self.queue_private.id, - 'submitter_email': 'ticket2.submitter@example.com', - 'body': 'Test ticket body', - 'priority': 3, + "title": "Private ticket test", + "queue": self.queue_private.id, + "submitter_email": "ticket2.submitter@example.com", + "body": "Test ticket body", + "priority": 3, } - response = self.client.post(reverse('helpdesk:home'), post_data) + response = self.client.post(reverse("helpdesk:home"), post_data) self.assertEqual(response.status_code, 200) self.assertEqual(email_count, len(mail.outbox)) - self.assertContains(response, 'Select a valid choice.') + self.assertContains(response, "Select a valid choice.") def test_create_ticket_customfields(self): email_count = len(mail.outbox) queue_custom = Queue.objects.create( - title='Queue 3', - slug='q3', + title="Queue 3", + slug="q3", allow_public_submission=True, - updated_ticket_cc='update.custom@example.com') + updated_ticket_cc="update.custom@example.com", + ) custom_field_1 = CustomField.objects.create( - name='textfield', - label='Text Field', - data_type='varchar', + name="textfield", + label="Text Field", + data_type="varchar", max_length=100, ordering=10, required=False, - staff_only=False) + staff_only=False, + ) post_data = { - 'queue': queue_custom.id, - 'title': 'Ticket with custom text field', - 'submitter_email': 'ticket3.submitter@example.com', - 'body': 'Test ticket body', - 'priority': 3, - 'custom_textfield': 'This is my custom text.', + "queue": queue_custom.id, + "title": "Ticket with custom text field", + "submitter_email": "ticket3.submitter@example.com", + "body": "Test ticket body", + "priority": 3, + "custom_textfield": "This is my custom text.", } - response = self.client.post( - reverse('helpdesk:home'), post_data, follow=True) + response = self.client.post(reverse("helpdesk:home"), post_data, follow=True) custom_field_1.delete() last_redirect = response.redirect_chain[-1] @@ -194,7 +202,7 @@ class TicketBasicsTestCase(TestCase): # Django 1.9 compatible way of testing this # https://docs.djangoproject.com/en/1.9/releases/1.9/#http-redirects-no-longer-forced-to-absolute-uris urlparts = urlparse(last_redirect_url) - self.assertEqual(urlparts.path, reverse('helpdesk:public_view')) + self.assertEqual(urlparts.path, reverse("helpdesk:public_view")) # Ensure only two e-mails were sent - submitter & updated. self.assertEqual(email_count + 2, len(mail.outbox)) @@ -209,15 +217,14 @@ class TicketBasicsTestCase(TestCase): self.queue_public.save() post_data = { - 'title': 'Test ticket title', - 'queue': self.queue_public.id, - 'submitter_email': 'queue@example.com', - 'body': 'Test ticket body', - 'priority': 3, + "title": "Test ticket title", + "queue": self.queue_public.id, + "submitter_email": "queue@example.com", + "body": "Test ticket body", + "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] @@ -226,39 +233,39 @@ class TicketBasicsTestCase(TestCase): # Django 1.9 compatible way of testing this # https://docs.djangoproject.com/en/1.9/releases/1.9/#http-redirects-no-longer-forced-to-absolute-uris urlparts = urlparse(last_redirect_url) - self.assertEqual(urlparts.path, reverse('helpdesk:public_view')) + self.assertEqual(urlparts.path, reverse("helpdesk:public_view")) # Ensure submitter, new-queue + update-queue were all emailed. self.assertEqual(email_count + 2, len(mail.outbox)) class EmailInteractionsTestCase(TestCase): - fixtures = ['emailtemplate.json'] + fixtures = ["emailtemplate.json"] def setUp(self): self.queue_public = Queue.objects.create( - title='Mail Queue 1', - slug='mq1', - email_address='queue-1@example.com', + title="Mail Queue 1", + slug="mq1", + email_address="queue-1@example.com", allow_public_submission=True, - new_ticket_cc='new.public.with.notifications@example.com', - updated_ticket_cc='update.public.with.notifications@example.com', + new_ticket_cc="new.public.with.notifications@example.com", + updated_ticket_cc="update.public.with.notifications@example.com", enable_notifications_on_email_events=True, ) self.queue_public_with_notifications_disabled = Queue.objects.create( - title='Mail Queue 2', - slug='mq2', - email_address='queue-2@example.com', + title="Mail Queue 2", + slug="mq2", + email_address="queue-2@example.com", allow_public_submission=True, - new_ticket_cc='new.public.without.notifications@example.com', - updated_ticket_cc='update.public.without.notifications@example.com', + new_ticket_cc="new.public.without.notifications@example.com", + updated_ticket_cc="update.public.without.notifications@example.com", enable_notifications_on_email_events=False, ) self.ticket_data = { - 'title': 'Test Ticket', - 'description': 'Some Test Ticket', + "title": "Test Ticket", + "description": "Some Test Ticket", } def test_create_ticket_from_email_with_message_id(self): @@ -271,14 +278,14 @@ class EmailInteractionsTestCase(TestCase): msg = email.message.Message() message_id = uuid.uuid4().hex - submitter_email = 'foo@bar.py' + submitter_email = "foo@bar.py" - msg.__setitem__('Message-ID', message_id) - msg.__setitem__('Subject', self.ticket_data['title']) - msg.__setitem__('From', submitter_email) - msg.__setitem__('To', self.queue_public.email_address) - msg.__setitem__('Content-Type', 'text/plain;') - msg.set_payload(self.ticket_data['description']) + msg.__setitem__("Message-ID", message_id) + msg.__setitem__("Subject", self.ticket_data["title"]) + msg.__setitem__("From", submitter_email) + msg.__setitem__("To", self.queue_public.email_address) + msg.__setitem__("Content-Type", "text/plain;") + msg.set_payload(self.ticket_data["description"]) email_count = len(mail.outbox) @@ -304,20 +311,23 @@ class EmailInteractionsTestCase(TestCase): """ msg = email.message.Message() - submitter_email = 'foo@bar.py' + submitter_email = "foo@bar.py" - msg.__setitem__('Subject', self.ticket_data['title']) - msg.__setitem__('From', submitter_email) - msg.__setitem__('To', self.queue_public.email_address) - msg.__setitem__('Content-Type', 'text/plain;') - msg.set_payload(self.ticket_data['description']) + msg.__setitem__("Subject", self.ticket_data["title"]) + msg.__setitem__("From", submitter_email) + msg.__setitem__("To", self.queue_public.email_address) + msg.__setitem__("Content-Type", "text/plain;") + msg.set_payload(self.ticket_data["description"]) email_count = len(mail.outbox) extract_email_metadata(str(msg), self.queue_public, logger=logger) ticket = Ticket.objects.get( - title=self.ticket_data['title'], queue=self.queue_public, submitter_email=submitter_email) + title=self.ticket_data["title"], + queue=self.queue_public, + submitter_email=submitter_email, + ) self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id) @@ -337,16 +347,16 @@ class EmailInteractionsTestCase(TestCase): msg = email.message.Message() message_id = uuid.uuid4().hex - submitter_email = 'foo@bar.py' - cc_list = ['bravo@example.net', 'charlie@foobar.com'] + submitter_email = "foo@bar.py" + cc_list = ["bravo@example.net", "charlie@foobar.com"] - msg.__setitem__('Message-ID', message_id) - msg.__setitem__('Subject', self.ticket_data['title']) - msg.__setitem__('From', submitter_email) - msg.__setitem__('To', self.queue_public.email_address) - msg.__setitem__('Cc', ','.join(cc_list)) - msg.__setitem__('Content-Type', 'text/plain;') - msg.set_payload(self.ticket_data['description']) + msg.__setitem__("Message-ID", message_id) + msg.__setitem__("Subject", self.ticket_data["title"]) + msg.__setitem__("From", submitter_email) + msg.__setitem__("To", self.queue_public.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) @@ -366,7 +376,6 @@ class EmailInteractionsTestCase(TestCase): self.assertIn(submitter_email, mail.outbox[0].to) for cc_email in cc_list: - # Ensure that contacts on cc_list will be notified on the same email (index 0) # self.assertIn(cc_email, mail.outbox[0].to) @@ -384,16 +393,16 @@ class EmailInteractionsTestCase(TestCase): msg = email.message.Message() message_id = uuid.uuid4().hex - submitter_email = 'foo@bar.py' + submitter_email = "foo@bar.py" to_list = [self.queue_public.email_address] - cc_list = ['bravo@example.net', 'charlie@foobar.com'] + cc_list = ["bravo@example.net", "charlie@foobar.com"] - msg.__setitem__('Message-ID', message_id) - msg.__setitem__('Subject', self.ticket_data['title']) - msg.__setitem__('From', submitter_email) - msg.__setitem__('To', ','.join(to_list + cc_list)) - msg.__setitem__('Content-Type', 'text/plain;') - msg.set_payload(self.ticket_data['description']) + msg.__setitem__("Message-ID", message_id) + msg.__setitem__("Subject", self.ticket_data["title"]) + msg.__setitem__("From", submitter_email) + msg.__setitem__("To", ",".join(to_list + cc_list)) + msg.__setitem__("Content-Type", "text/plain;") + msg.set_payload(self.ticket_data["description"]) email_count = len(mail.outbox) @@ -414,11 +423,11 @@ class EmailInteractionsTestCase(TestCase): # 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]) + self.assertRaises( + TicketCC.DoesNotExist, TicketCC.objects.get, ticket=ticket, email=to_list[0] + ) for cc_email in cc_list: - # Ensure that contacts on cc_list will be notified on the same email (index 0) # self.assertIn(cc_email, mail.outbox[0].to) @@ -436,16 +445,16 @@ class EmailInteractionsTestCase(TestCase): msg = email.message.Message() message_id = uuid.uuid4().hex - submitter_email = 'foo@bar.py' - cc_list = ['null@example', 'invalid@foobar'] + submitter_email = "foo@bar.py" + cc_list = ["null@example", "invalid@foobar"] - msg.__setitem__('Message-ID', message_id) - msg.__setitem__('Subject', self.ticket_data['title']) - msg.__setitem__('From', submitter_email) - msg.__setitem__('To', self.queue_public.email_address) - msg.__setitem__('Cc', ','.join(cc_list)) - msg.__setitem__('Content-Type', 'text/plain;') - msg.set_payload(self.ticket_data['description']) + msg.__setitem__("Message-ID", message_id) + msg.__setitem__("Subject", self.ticket_data["title"]) + msg.__setitem__("From", submitter_email) + msg.__setitem__("To", self.queue_public.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) @@ -465,7 +474,6 @@ class EmailInteractionsTestCase(TestCase): self.assertIn(submitter_email, mail.outbox[0].to) for cc_email in cc_list: - # Ensure that contacts on cc_list will be notified on the same email (index 0) # self.assertIn(cc_email, mail.outbox[0].to) @@ -474,7 +482,9 @@ class EmailInteractionsTestCase(TestCase): self.assertTrue(ticket_cc.ticket, ticket) self.assertTrue(ticket_cc.email, cc_email) - def test_create_followup_from_email_with_valid_message_id_with_no_initial_cc_list(self): + def test_create_followup_from_email_with_valid_message_id_with_no_initial_cc_list( + self, + ): """ Ensure that if a message is received with an valid In-Reply-To ID, the expected instances are created even if the there were @@ -484,14 +494,14 @@ class EmailInteractionsTestCase(TestCase): msg = email.message.Message() message_id = uuid.uuid4().hex - submitter_email = 'foo@bar.py' + submitter_email = "foo@bar.py" - msg.__setitem__('Message-ID', message_id) - msg.__setitem__('Subject', self.ticket_data['title']) - msg.__setitem__('From', submitter_email) - msg.__setitem__('To', self.queue_public.email_address) - msg.__setitem__('Content-Type', 'text/plain;') - msg.set_payload(self.ticket_data['description']) + msg.__setitem__("Message-ID", message_id) + msg.__setitem__("Subject", self.ticket_data["title"]) + msg.__setitem__("From", submitter_email) + msg.__setitem__("To", self.queue_public.email_address) + msg.__setitem__("Content-Type", "text/plain;") + msg.set_payload(self.ticket_data["description"]) email_count = len(mail.outbox) @@ -517,17 +527,17 @@ class EmailInteractionsTestCase(TestCase): reply = email.message.Message() reply_message_id = uuid.uuid4().hex - submitter_email = 'foo@bar.py' - cc_list = ['bravo@example.net', 'charlie@foobar.com'] + submitter_email = "foo@bar.py" + cc_list = ["bravo@example.net", "charlie@foobar.com"] - reply.__setitem__('Message-ID', reply_message_id) - 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.email_address) - reply.__setitem__('Cc', ','.join(cc_list)) - reply.__setitem__('Content-Type', 'text/plain;') - reply.set_payload(self.ticket_data['description']) + reply.__setitem__("Message-ID", reply_message_id) + 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.email_address) + reply.__setitem__("Cc", ",".join(cc_list)) + reply.__setitem__("Content-Type", "text/plain;") + reply.set_payload(self.ticket_data["description"]) extract_email_metadata(str(reply), self.queue_public, logger=logger) @@ -562,7 +572,9 @@ class EmailInteractionsTestCase(TestCase): # for cc_email in cc_list: # self.assertIn(cc_email, mail.outbox[expected_email_count - 1].to) - def test_create_followup_from_email_with_valid_message_id_with_original_cc_list_included(self): + def test_create_followup_from_email_with_valid_message_id_with_original_cc_list_included( + self, + ): """ Ensure that if a message is received with an valid In-Reply-To ID, the expected instances are created but if there's any @@ -572,16 +584,16 @@ class EmailInteractionsTestCase(TestCase): msg = email.message.Message() message_id = uuid.uuid4().hex - submitter_email = 'foo@bar.py' - cc_list = ['bravo@example.net', 'charlie@foobar.com'] + submitter_email = "foo@bar.py" + cc_list = ["bravo@example.net", "charlie@foobar.com"] - msg.__setitem__('Message-ID', message_id) - msg.__setitem__('Subject', self.ticket_data['title']) - msg.__setitem__('From', submitter_email) - msg.__setitem__('To', self.queue_public.email_address) - msg.__setitem__('Cc', ','.join(cc_list)) - msg.__setitem__('Content-Type', 'text/plain;') - msg.set_payload(self.ticket_data['description']) + msg.__setitem__("Message-ID", message_id) + msg.__setitem__("Subject", self.ticket_data["title"]) + msg.__setitem__("From", submitter_email) + msg.__setitem__("To", self.queue_public.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) @@ -617,17 +629,17 @@ class EmailInteractionsTestCase(TestCase): reply = email.message.Message() reply_message_id = uuid.uuid4().hex - submitter_email = 'foo@bar.py' - cc_list = ['bravo@example.net', 'charlie@foobar.com'] + submitter_email = "foo@bar.py" + cc_list = ["bravo@example.net", "charlie@foobar.com"] - reply.__setitem__('Message-ID', reply_message_id) - 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.email_address) - reply.__setitem__('Cc', ','.join(cc_list)) - reply.__setitem__('Content-Type', 'text/plain;') - reply.set_payload(self.ticket_data['description']) + reply.__setitem__("Message-ID", reply_message_id) + 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.email_address) + reply.__setitem__("Cc", ",".join(cc_list)) + reply.__setitem__("Content-Type", "text/plain;") + reply.set_payload(self.ticket_data["description"]) extract_email_metadata(str(reply), self.queue_public, logger=logger) @@ -670,16 +682,16 @@ class EmailInteractionsTestCase(TestCase): msg = email.message.Message() message_id = uuid.uuid4().hex - submitter_email = 'foo@bar.py' - cc_list = ['bravo@example.net', 'charlie@foobar.com'] + submitter_email = "foo@bar.py" + cc_list = ["bravo@example.net", "charlie@foobar.com"] - msg.__setitem__('Message-ID', message_id) - msg.__setitem__('Subject', self.ticket_data['title']) - msg.__setitem__('From', submitter_email) - msg.__setitem__('To', self.queue_public.email_address) - msg.__setitem__('Cc', ','.join(cc_list)) - msg.__setitem__('Content-Type', 'text/plain;') - msg.set_payload(self.ticket_data['description']) + msg.__setitem__("Message-ID", message_id) + msg.__setitem__("Subject", self.ticket_data["title"]) + msg.__setitem__("From", submitter_email) + msg.__setitem__("To", self.queue_public.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) @@ -707,7 +719,6 @@ class EmailInteractionsTestCase(TestCase): # Ensure that is created for cc_email in cc_list: - # Ensure that contacts on cc_list will be notified on the same email (index 0) # self.assertIn(cc_email, mail.outbox[0].to) @@ -721,20 +732,20 @@ class EmailInteractionsTestCase(TestCase): reply = email.message.Message() reply_message_id = uuid.uuid4().hex - submitter_email = 'foo@bar.py' - cc_list = ['bravo@example.net', 'charlie@foobar.com'] + submitter_email = "foo@bar.py" + cc_list = ["bravo@example.net", "charlie@foobar.com"] - invalid_message_id = 'INVALID' - reply_subject = 'Re: ' + self.ticket_data['title'] + invalid_message_id = "INVALID" + reply_subject = "Re: " + self.ticket_data["title"] - reply.__setitem__('Message-ID', reply_message_id) - reply.__setitem__('In-Reply-To', invalid_message_id) - reply.__setitem__('Subject', reply_subject) - reply.__setitem__('From', submitter_email) - reply.__setitem__('To', self.queue_public.email_address) - reply.__setitem__('Cc', ','.join(cc_list)) - reply.__setitem__('Content-Type', 'text/plain;') - reply.set_payload(self.ticket_data['description']) + reply.__setitem__("Message-ID", reply_message_id) + reply.__setitem__("In-Reply-To", invalid_message_id) + reply.__setitem__("Subject", reply_subject) + reply.__setitem__("From", submitter_email) + reply.__setitem__("To", self.queue_public.email_address) + reply.__setitem__("Cc", ",".join(cc_list)) + reply.__setitem__("Content-Type", "text/plain;") + reply.set_payload(self.ticket_data["description"]) email_count = len(mail.outbox) @@ -760,24 +771,24 @@ class EmailInteractionsTestCase(TestCase): def test_create_ticket_from_email_to_a_notification_enabled_queue(self): """ - Ensure that when an email is sent to a Queue with - notifications_enabled turned ON, and a is created, all - contacts in the TicketCC list are notified. + Ensure that when an email is sent to a Queue with + notifications_enabled turned ON, and a is created, all + contacts in the TicketCC list are notified. """ msg = email.message.Message() message_id = uuid.uuid4().hex - submitter_email = 'foo@bar.py' - cc_list = ['bravo@example.net', 'charlie@foobar.com'] + submitter_email = "foo@bar.py" + cc_list = ["bravo@example.net", "charlie@foobar.com"] - msg.__setitem__('Message-ID', message_id) - msg.__setitem__('Subject', self.ticket_data['title']) - msg.__setitem__('From', submitter_email) - msg.__setitem__('To', self.queue_public.email_address) - msg.__setitem__('Cc', ','.join(cc_list)) - msg.__setitem__('Content-Type', 'text/plain;') - msg.set_payload(self.ticket_data['description']) + msg.__setitem__("Message-ID", message_id) + msg.__setitem__("Subject", self.ticket_data["title"]) + msg.__setitem__("From", submitter_email) + msg.__setitem__("To", self.queue_public.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) extract_email_metadata(str(msg), self.queue_public, logger=logger) @@ -797,7 +808,6 @@ class EmailInteractionsTestCase(TestCase): # Ensure that exist for cc_email in cc_list: - # Ensure that contacts on cc_list will be notified on the same email (index 0) # self.assertIn(cc_email, mail.outbox[0].to) @@ -807,31 +817,33 @@ class EmailInteractionsTestCase(TestCase): def test_create_ticket_from_email_to_a_notification_disabled_queue(self): """ - Ensure that when an email is sent to a Queue with notifications_enabled - turned OFF, only the new_ticket_cc and updated_ticket_cc contacts (if - they are set) are notified. No contact from the TicketCC list should - be notified. + Ensure that when an email is sent to a Queue with notifications_enabled + turned OFF, only the new_ticket_cc and updated_ticket_cc contacts (if + they are set) are notified. No contact from the TicketCC list should + be notified. """ msg = email.message.Message() message_id = uuid.uuid4().hex - submitter_email = 'foo@bar.py' - cc_list = ['bravo@example.net', 'charlie@foobar.com'] + submitter_email = "foo@bar.py" + cc_list = ["bravo@example.net", "charlie@foobar.com"] - msg.__setitem__('Message-ID', message_id) - msg.__setitem__('Subject', self.ticket_data['title']) - msg.__setitem__('From', submitter_email) + 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__('Cc', ','.join(cc_list)) - msg.__setitem__('Content-Type', 'text/plain;') - msg.set_payload(self.ticket_data['description']) + "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) extract_email_metadata( - str(msg), self.queue_public_with_notifications_disabled, logger=logger) + 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) @@ -846,7 +858,6 @@ class EmailInteractionsTestCase(TestCase): # Ensure that is created even if the Queue notifications are disabled # so when staff members interact with the , they get notified for cc_email in cc_list: - # Ensure that contacts on the cc_list are not notified self.assertNotIn(cc_email, mail.outbox[0].to) @@ -856,24 +867,24 @@ class EmailInteractionsTestCase(TestCase): def test_create_followup_from_email_to_a_notification_enabled_queue(self): """ - Ensure that when an email is sent to a Queue with notifications_enabled - turned ON, and a is created, all contacts n the TicketCC - list are notified. + Ensure that when an email is sent to a Queue with notifications_enabled + turned ON, and a is created, all contacts n the TicketCC + list are notified. """ # Ticket and TicketCCs creation # msg = email.message.Message() message_id = uuid.uuid4().hex - submitter_email = 'foo@bar.py' - cc_list = ['bravo@example.net', 'charlie@foobar.com'] + submitter_email = "foo@bar.py" + cc_list = ["bravo@example.net", "charlie@foobar.com"] - msg.__setitem__('Message-ID', message_id) - msg.__setitem__('Subject', self.ticket_data['title']) - msg.__setitem__('From', submitter_email) - msg.__setitem__('To', self.queue_public.email_address) - msg.__setitem__('Cc', ','.join(cc_list)) - msg.__setitem__('Content-Type', 'text/plain;') - msg.set_payload(self.ticket_data['description']) + msg.__setitem__("Message-ID", message_id) + msg.__setitem__("Subject", self.ticket_data["title"]) + msg.__setitem__("From", submitter_email) + msg.__setitem__("To", self.queue_public.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) @@ -892,7 +903,6 @@ class EmailInteractionsTestCase(TestCase): # Ensure that is created for cc_email in cc_list: - # Ensure that contacts on cc_list will be notified on the same email (index 0) # self.assertIn(cc_email, mail.outbox[0].to) @@ -905,15 +915,15 @@ class EmailInteractionsTestCase(TestCase): reply = email.message.Message() reply_message_id = uuid.uuid4().hex - submitter_email = 'bravo@example.net' + submitter_email = "bravo@example.net" - reply.__setitem__('Message-ID', reply_message_id) - 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.email_address) - reply.__setitem__('Content-Type', 'text/plain;') - reply.set_payload(self.ticket_data['description']) + reply.__setitem__("Message-ID", reply_message_id) + 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.email_address) + reply.__setitem__("Content-Type", "text/plain;") + reply.set_payload(self.ticket_data["description"]) extract_email_metadata(str(reply), self.queue_public, logger=logger) @@ -930,7 +940,6 @@ class EmailInteractionsTestCase(TestCase): # Ensure that exist for cc_email in cc_list: - # Ensure that contacts on cc_list will be notified on the same email (index 0) # self.assertIn(cc_email, mail.outbox[expected_email_count - 1].to) @@ -940,29 +949,31 @@ class EmailInteractionsTestCase(TestCase): def test_create_followup_from_email_to_a_notification_disabled_queue(self): """ - Ensure that when an email is sent to a Queue with notifications_enabled - turned OFF, and a is created, TicketCC is NOT notified. + Ensure that when an email is sent to a Queue with notifications_enabled + turned OFF, and a is created, TicketCC is NOT notified. """ # Ticket and TicketCCs creation # msg = email.message.Message() message_id = uuid.uuid4().hex - submitter_email = 'foo@bar.py' - cc_list = ['bravo@example.net', 'charlie@foobar.com'] + submitter_email = "foo@bar.py" + cc_list = ["bravo@example.net", "charlie@foobar.com"] - msg.__setitem__('Message-ID', message_id) - msg.__setitem__('Subject', self.ticket_data['title']) - msg.__setitem__('From', submitter_email) + 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__('Cc', ','.join(cc_list)) - msg.__setitem__('Content-Type', 'text/plain;') - msg.set_payload(self.ticket_data['description']) + "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) extract_email_metadata( - str(msg), self.queue_public_with_notifications_disabled, logger=logger) + 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) @@ -976,7 +987,6 @@ class EmailInteractionsTestCase(TestCase): # Ensure that is created for cc_email in cc_list: - # Ensure that contacts on cc_list will not be notified self.assertNotIn(cc_email, mail.outbox[0].to) @@ -989,19 +999,21 @@ class EmailInteractionsTestCase(TestCase): reply = email.message.Message() reply_message_id = uuid.uuid4().hex - submitter_email = 'bravo@example.net' + submitter_email = "bravo@example.net" - reply.__setitem__('Message-ID', reply_message_id) - reply.__setitem__('In-Reply-To', message_id) - reply.__setitem__('Subject', self.ticket_data['title']) - reply.__setitem__('From', submitter_email) + reply.__setitem__("Message-ID", reply_message_id) + 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__('Content-Type', 'text/plain;') - reply.set_payload(self.ticket_data['description']) + "To", self.queue_public_with_notifications_disabled.email_address + ) + reply.__setitem__("Content-Type", "text/plain;") + reply.set_payload(self.ticket_data["description"]) extract_email_metadata( - str(reply), self.queue_public_with_notifications_disabled, logger=logger) + 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) @@ -1023,14 +1035,14 @@ class EmailInteractionsTestCase(TestCase): msg = email.message.Message() message_id = uuid.uuid4().hex - submitter_email = 'foo@bar.py' + submitter_email = "foo@bar.py" - msg.__setitem__('Message-ID', message_id) - msg.__setitem__('Subject', self.ticket_data['title']) - msg.__setitem__('From', submitter_email) - msg.__setitem__('To', self.queue_public.email_address) - msg.__setitem__('Content-Type', 'text/plain;') - msg.set_payload(self.ticket_data['description']) + msg.__setitem__("Message-ID", message_id) + msg.__setitem__("Subject", self.ticket_data["title"]) + msg.__setitem__("From", submitter_email) + msg.__setitem__("To", self.queue_public.email_address) + msg.__setitem__("Content-Type", "text/plain;") + msg.set_payload(self.ticket_data["description"]) email_count = len(mail.outbox) @@ -1044,17 +1056,17 @@ class EmailInteractionsTestCase(TestCase): reply = email.message.Message() reply_message_id = uuid.uuid4().hex - submitter_email = 'bravo@example.net' - cc_list = ['foo@bar.py', 'charlie@foobar.com'] + submitter_email = "bravo@example.net" + cc_list = ["foo@bar.py", "charlie@foobar.com"] - reply.__setitem__('Message-ID', reply_message_id) - 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.email_address) - reply.__setitem__('Cc', ','.join(cc_list)) - reply.__setitem__('Content-Type', 'text/plain;') - reply.set_payload(self.ticket_data['description']) + reply.__setitem__("Message-ID", reply_message_id) + 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.email_address) + reply.__setitem__("Cc", ",".join(cc_list)) + reply.__setitem__("Content-Type", "text/plain;") + reply.set_payload(self.ticket_data["description"]) extract_email_metadata(str(reply), self.queue_public, logger=logger) @@ -1085,7 +1097,7 @@ class EmailInteractionsTestCase(TestCase): cat = KBCategory.objects.create( title="Test Cat", slug="test_cat", - description="This is a test category", + description="This is a test category", queue=self.queue_public, ) cat.save() @@ -1105,10 +1117,21 @@ class EmailInteractionsTestCase(TestCase): answer="A KB Item", ) self.kbitem1.save() - cat_url = reverse('helpdesk:submit') + '?' \ - + attr_list["f1_attr"] + '=' + attr_list["f1_attr_value"] + '&' \ - + attr_list["f2_attr"] + '=' + attr_list["f2_attr_value"] + '&' \ - + attr_list["f3_attr"] + '=' + attr_list["f3_attr_value"] + cat_url = ( + reverse("helpdesk:submit") + + "?" + + attr_list["f1_attr"] + + "=" + + attr_list["f1_attr_value"] + + "&" + + attr_list["f2_attr"] + + "=" + + attr_list["f2_attr_value"] + + "&" + + attr_list["f3_attr"] + + "=" + + attr_list["f3_attr_value"] + ) response = self.client.get(cat_url) # Get the rendered response to make it easier to debug if things go wrong if ( @@ -1122,11 +1145,29 @@ class EmailInteractionsTestCase(TestCase): else: content = response.content - msg_prefix = content.decode(response.charset) self.assertContains( - response, '', msg_prefix = msg_prefix) + response, + '", + msg_prefix=msg_prefix, + ) self.assertContains( - response, ' self.queue_public.time_spent.seconds + self.queue_public.dedicated_time.seconds + > self.queue_public.time_spent.seconds ) diff --git a/helpdesk/tests/test_time_spent_auto.py b/helpdesk/tests/test_time_spent_auto.py index 56b25ad0..fc78f2df 100644 --- a/helpdesk/tests/test_time_spent_auto.py +++ b/helpdesk/tests/test_time_spent_auto.py @@ -1,4 +1,3 @@ - from datetime import datetime, timedelta from django.contrib.auth.hashers import make_password from django.contrib.auth.models import User @@ -13,43 +12,42 @@ import uuid @override_settings(USE_TZ=True) class TimeSpentAutoTestCase(TestCase): - def setUp(self): """Creates a queue, ticket and user.""" self.queue_public = Queue.objects.create( - title='Queue 1', - slug='q1', + title="Queue 1", + slug="q1", allow_public_submission=True, - dedicated_time=timedelta(minutes=60) + dedicated_time=timedelta(minutes=60), ) - self.ticket_data = dict(queue=self.queue_public, - title='test ticket', - description='test ticket description') + self.ticket_data = dict( + queue=self.queue_public, + title="test ticket", + description="test ticket description", + ) self.user = User.objects.create( - username='staff', - email='staff@example.com', - password=make_password('Test1234'), + username="staff", + email="staff@example.com", + password=make_password("Test1234"), is_staff=True, is_superuser=False, - is_active=True + is_active=True, ) self.client = Client() - def loginUser(self, is_staff=True): """Create a staff user and login""" User = get_user_model() self.user = User.objects.create( - username='User_1', + username="User_1", is_staff=is_staff, ) - self.user.set_password('pass') + self.user.set_password("pass") self.user.save() - self.client.login(username='User_1', password='pass') - + self.client.login(username="User_1", password="pass") def test_add_two_followups_time_spent_auto(self): """Tests automatic time_spent calculation.""" @@ -59,15 +57,39 @@ class TimeSpentAutoTestCase(TestCase): # ticket creation date, follow-up creation date, assertion value TEST_VALUES = ( # friday - ('2024-03-01T00:00:00+00:00', '2024-03-01T09:30:10+00:00', timedelta(hours=9, minutes=30, seconds=10)), - ('2024-03-01T00:00:00+00:00', '2024-03-01T23:59:58+00:00', timedelta(hours=23, minutes=59, seconds=58)), - ('2024-03-01T00:00:00+00:00', '2024-03-01T23:59:59+00:00', timedelta(hours=23, minutes=59, seconds=59)), - ('2024-03-01T00:00:00+00:00', '2024-03-02T00:00:00+00:00', timedelta(hours=24)), - ('2024-03-01T00:00:00+00:00', '2024-03-02T09:00:00+00:00', timedelta(hours=33)), - ('2024-03-01T00:00:00+00:00', '2024-03-03T00:00:00+00:00', timedelta(hours=48)), + ( + "2024-03-01T00:00:00+00:00", + "2024-03-01T09:30:10+00:00", + timedelta(hours=9, minutes=30, seconds=10), + ), + ( + "2024-03-01T00:00:00+00:00", + "2024-03-01T23:59:58+00:00", + timedelta(hours=23, minutes=59, seconds=58), + ), + ( + "2024-03-01T00:00:00+00:00", + "2024-03-01T23:59:59+00:00", + timedelta(hours=23, minutes=59, seconds=59), + ), + ( + "2024-03-01T00:00:00+00:00", + "2024-03-02T00:00:00+00:00", + timedelta(hours=24), + ), + ( + "2024-03-01T00:00:00+00:00", + "2024-03-02T09:00:00+00:00", + timedelta(hours=33), + ), + ( + "2024-03-01T00:00:00+00:00", + "2024-03-03T00:00:00+00:00", + timedelta(hours=48), + ), ) - for (ticket_time, fup_time, assertion_delta) in TEST_VALUES: + for ticket_time, fup_time, assertion_delta in TEST_VALUES: # create and setup test ticket time ticket = Ticket.objects.create(**self.ticket_data) ticket_time_p = datetime.strptime(ticket_time, "%Y-%m-%dT%H:%M:%S%z") @@ -85,15 +107,24 @@ class TimeSpentAutoTestCase(TestCase): user=self.user, new_status=1, message_id=uuid.uuid4().hex, - time_spent=None + time_spent=None, ) - self.assertEqual(followup1.time_spent.total_seconds(), assertion_delta.total_seconds()) - self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds()) + self.assertEqual( + followup1.time_spent.total_seconds(), assertion_delta.total_seconds() + ) + self.assertEqual( + ticket.time_spent.total_seconds(), assertion_delta.total_seconds() + ) # adding a second follow-up at different intervals - for delta in (timedelta(seconds=1), timedelta(minutes=1), timedelta(hours=1), timedelta(days=1), timedelta(days=10)): - + for delta in ( + timedelta(seconds=1), + timedelta(minutes=1), + timedelta(hours=1), + timedelta(days=1), + timedelta(days=10), + ): followup2 = FollowUp.objects.create( ticket=ticket, date=followup1.date + delta, @@ -103,16 +134,20 @@ class TimeSpentAutoTestCase(TestCase): user=self.user, new_status=1, message_id=uuid.uuid4().hex, - time_spent=None + time_spent=None, ) - self.assertEqual(followup2.time_spent.total_seconds(), delta.total_seconds()) - self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds() + delta.total_seconds()) + self.assertEqual( + followup2.time_spent.total_seconds(), delta.total_seconds() + ) + self.assertEqual( + ticket.time_spent.total_seconds(), + assertion_delta.total_seconds() + delta.total_seconds(), + ) # delete second follow-up as we test it with many intervals followup2.delete() - def test_followup_time_spent_auto_opening_hours(self): """Tests automatic time_spent calculation with opening hours and holidays.""" @@ -130,45 +165,118 @@ class TimeSpentAutoTestCase(TestCase): # adding holidays helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = ( - '2024-03-18', '2024-03-19', '2024-03-20', '2024-03-21', '2024-03-22', + "2024-03-18", + "2024-03-19", + "2024-03-20", + "2024-03-21", + "2024-03-22", ) # ticket creation date, follow-up creation date, assertion value TEST_VALUES = ( # monday - ('2024-03-04T00:00:00+00:00', '2024-03-04T09:30:10+00:00', timedelta(hours=9, minutes=30, seconds=10)), + ( + "2024-03-04T00:00:00+00:00", + "2024-03-04T09:30:10+00:00", + timedelta(hours=9, minutes=30, seconds=10), + ), # tuesday - ('2024-03-05T07:00:00+00:00', '2024-03-05T09:00:00+00:00', timedelta(hours=1)), - ('2024-03-05T17:50:00+00:00', '2024-03-05T17:51:00+00:00', timedelta(minutes=1)), - ('2024-03-05T17:50:00+00:00', '2024-03-05T19:51:00+00:00', timedelta(minutes=10)), - ('2024-03-05T18:00:00+00:00', '2024-03-05T23:59:59+00:00', timedelta(hours=0)), - ('2024-03-05T20:00:00+00:00', '2024-03-05T20:59:59+00:00', timedelta(hours=0)), + ( + "2024-03-05T07:00:00+00:00", + "2024-03-05T09:00:00+00:00", + timedelta(hours=1), + ), + ( + "2024-03-05T17:50:00+00:00", + "2024-03-05T17:51:00+00:00", + timedelta(minutes=1), + ), + ( + "2024-03-05T17:50:00+00:00", + "2024-03-05T19:51:00+00:00", + timedelta(minutes=10), + ), + ( + "2024-03-05T18:00:00+00:00", + "2024-03-05T23:59:59+00:00", + timedelta(hours=0), + ), + ( + "2024-03-05T20:00:00+00:00", + "2024-03-05T20:59:59+00:00", + timedelta(hours=0), + ), # wednesday - ('2024-03-06T08:00:00+00:00', '2024-03-06T09:01:00+00:00', timedelta(minutes=31)), - ('2024-03-06T01:00:00+00:00', '2024-03-06T19:30:10+00:00', timedelta(hours=10)), - ('2024-03-06T18:01:00+00:00', '2024-03-06T19:00:00+00:00', timedelta(minutes=29)), + ( + "2024-03-06T08:00:00+00:00", + "2024-03-06T09:01:00+00:00", + timedelta(minutes=31), + ), + ( + "2024-03-06T01:00:00+00:00", + "2024-03-06T19:30:10+00:00", + timedelta(hours=10), + ), + ( + "2024-03-06T18:01:00+00:00", + "2024-03-06T19:00:00+00:00", + timedelta(minutes=29), + ), # thursday - ('2024-03-07T00:00:00+00:00', '2024-03-07T09:30:10+00:00', timedelta(hours=9, minutes=30, seconds=10)), - ('2024-03-07T09:30:00+00:00', '2024-03-07T10:30:00+00:00', timedelta(minutes=30)), + ( + "2024-03-07T00:00:00+00:00", + "2024-03-07T09:30:10+00:00", + timedelta(hours=9, minutes=30, seconds=10), + ), + ( + "2024-03-07T09:30:00+00:00", + "2024-03-07T10:30:00+00:00", + timedelta(minutes=30), + ), # friday - ('2024-03-08T00:00:00+00:00', '2024-03-08T23:30:10+00:00', timedelta(hours=10)), + ( + "2024-03-08T00:00:00+00:00", + "2024-03-08T23:30:10+00:00", + timedelta(hours=10), + ), # saturday - ('2024-03-09T00:00:00+00:00', '2024-03-09T09:30:10+00:00', timedelta(hours=0)), + ( + "2024-03-09T00:00:00+00:00", + "2024-03-09T09:30:10+00:00", + timedelta(hours=0), + ), # sunday - ('2024-03-10T00:00:00+00:00', '2024-03-10T09:30:10+00:00', timedelta(hours=0)), - + ( + "2024-03-10T00:00:00+00:00", + "2024-03-10T09:30:10+00:00", + timedelta(hours=0), + ), # monday to sunday - ('2024-03-04T04:00:00+00:00', '2024-03-10T09:00:00+00:00', timedelta(hours=60)), - + ( + "2024-03-04T04:00:00+00:00", + "2024-03-10T09:00:00+00:00", + timedelta(hours=60), + ), # two weeks - ('2024-03-04T04:00:00+00:00', '2024-03-17T09:00:00+00:00', timedelta(hours=124)), - + ( + "2024-03-04T04:00:00+00:00", + "2024-03-17T09:00:00+00:00", + timedelta(hours=124), + ), # three weeks, the third one is holidays - ('2024-03-04T04:00:00+00:00', '2024-03-24T09:00:00+00:00', timedelta(hours=124)), - ('2024-03-18T04:00:00+00:00', '2024-03-24T09:00:00+00:00', timedelta(hours=0)), + ( + "2024-03-04T04:00:00+00:00", + "2024-03-24T09:00:00+00:00", + timedelta(hours=124), + ), + ( + "2024-03-18T04:00:00+00:00", + "2024-03-24T09:00:00+00:00", + timedelta(hours=0), + ), ) - for (ticket_time, fup_time, assertion_delta) in TEST_VALUES: + for ticket_time, fup_time, assertion_delta in TEST_VALUES: # create and setup test ticket time ticket = Ticket.objects.create(**self.ticket_data) ticket_time_p = datetime.strptime(ticket_time, "%Y-%m-%dT%H:%M:%S%z") @@ -186,11 +294,15 @@ class TimeSpentAutoTestCase(TestCase): user=self.user, new_status=1, message_id=uuid.uuid4().hex, - time_spent=None + time_spent=None, ) - self.assertEqual(followup1.time_spent.total_seconds(), assertion_delta.total_seconds()) - self.assertEqual(ticket.time_spent.total_seconds(), assertion_delta.total_seconds()) + self.assertEqual( + followup1.time_spent.total_seconds(), assertion_delta.total_seconds() + ) + self.assertEqual( + ticket.time_spent.total_seconds(), assertion_delta.total_seconds() + ) # removing opening hours and holidays helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS = {} @@ -205,15 +317,18 @@ class TimeSpentAutoTestCase(TestCase): # Follow-ups with OPEN_STATUS are excluded from time counting helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = (Ticket.OPEN_STATUS,) - # create and setup test ticket time ticket = Ticket.objects.create(**self.ticket_data) - ticket_time_p = datetime.strptime('2024-03-04T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z") + ticket_time_p = datetime.strptime( + "2024-03-04T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z" + ) ticket.created = ticket_time_p ticket.modified = ticket_time_p ticket.save() - fup_time_p = datetime.strptime('2024-03-10T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z") + fup_time_p = datetime.strptime( + "2024-03-10T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z" + ) followup1 = FollowUp.objects.create( ticket=ticket, date=fup_time_p, @@ -223,7 +338,7 @@ class TimeSpentAutoTestCase(TestCase): user=self.user, new_status=1, message_id=uuid.uuid4().hex, - time_spent=None + time_spent=None, ) # The Follow-up time_spent should be zero as the default OPEN_STATUS was excluded from calculation @@ -233,7 +348,6 @@ class TimeSpentAutoTestCase(TestCase): # Remove status exclusion helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = () - def test_followup_time_spent_auto_exclude_queues(self): """Tests automatic time_spent calculation queues exclusion.""" @@ -241,17 +355,20 @@ class TimeSpentAutoTestCase(TestCase): helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True # Follow-ups within the default queue are excluded from time counting - helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ('q1',) - + helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ("q1",) # create and setup test ticket time ticket = Ticket.objects.create(**self.ticket_data) - ticket_time_p = datetime.strptime('2024-03-04T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z") + ticket_time_p = datetime.strptime( + "2024-03-04T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z" + ) ticket.created = ticket_time_p ticket.modified = ticket_time_p ticket.save() - fup_time_p = datetime.strptime('2024-03-10T00:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z") + fup_time_p = datetime.strptime( + "2024-03-10T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z" + ) followup1 = FollowUp.objects.create( ticket=ticket, date=fup_time_p, @@ -261,7 +378,7 @@ class TimeSpentAutoTestCase(TestCase): user=self.user, new_status=1, message_id=uuid.uuid4().hex, - time_spent=None + time_spent=None, ) # The Follow-up time_spent should be zero as the default queue was excluded from calculation @@ -276,13 +393,13 @@ class TimeSpentAutoTestCase(TestCase): # activate automatic calculation helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO = True - helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ('stop1', 'stop2') + helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ("stop1", "stop2") # make staff user self.loginUser() # create queues - queues_sequence = ('new', 'stop1', 'resume1', 'stop2', 'resume2', 'end') + queues_sequence = ("new", "stop1", "resume1", "stop2", "resume2", "end") queues = dict() for slug in queues_sequence: queues[slug] = Queue.objects.create( @@ -292,34 +409,39 @@ class TimeSpentAutoTestCase(TestCase): # create ticket initial_data = { - 'title': 'Queue change ticket test', - 'queue': queues['new'], - 'assigned_to': self.user, - 'status': Ticket.OPEN_STATUS, - 'created': datetime.strptime('2024-04-09T08:00:00+00:00', "%Y-%m-%dT%H:%M:%S%z") + "title": "Queue change ticket test", + "queue": queues["new"], + "assigned_to": self.user, + "status": Ticket.OPEN_STATUS, + "created": datetime.strptime( + "2024-04-09T08:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z" + ), } ticket = Ticket.objects.create(**initial_data) # create a change queue follow-up every hour # first follow-up created at the same time of the ticket without queue change # new --1h--> stop1 --0h--> resume1 --1h--> stop2 --0h--> resume2 --1h--> end - for (i, queue) in enumerate(queues_sequence): + for i, queue in enumerate(queues_sequence): # create follow-up post_data = { - 'comment': 'ticket in queue {}'.format(queue), - 'queue': queues[queue].id, + "comment": "ticket in queue {}".format(queue), + "queue": queues[queue].id, } - response = self.client.post(reverse('helpdesk:update', kwargs={ - 'ticket_id': ticket.id}), post_data) - latest_fup = ticket.followup_set.latest('id') + response = self.client.post( + reverse("helpdesk:update", kwargs={"ticket_id": ticket.id}), post_data + ) + latest_fup = ticket.followup_set.latest("id") latest_fup.date = ticket.created + timedelta(hours=i) latest_fup.time_spent = None latest_fup.save() - + # total ticket time for followups is 5 hours self.assertEqual(latest_fup.date - ticket.created, timedelta(hours=5)) # calculated time spent with 2 hours exclusion is 3 hours - self.assertEqual(ticket.time_spent.total_seconds(), timedelta(hours=3).total_seconds()) + self.assertEqual( + ticket.time_spent.total_seconds(), timedelta(hours=3).total_seconds() + ) # remove queues exclusion - helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = () \ No newline at end of file + helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = () diff --git a/helpdesk/tests/test_usersettings.py b/helpdesk/tests/test_usersettings.py index 7ba26c96..1acccaca 100644 --- a/helpdesk/tests/test_usersettings.py +++ b/helpdesk/tests/test_usersettings.py @@ -4,20 +4,18 @@ from django.urls import reverse class TicketActionsTestCase(TestCase): - fixtures = ['emailtemplate.json'] + fixtures = ["emailtemplate.json"] def setUp(self): User = get_user_model() self.user = User.objects.create( - username='User_1', + username="User_1", is_staff=True, ) - self.user.set_password('pass') + self.user.set_password("pass") self.user.save() - self.client.login(username='User_1', password='pass') + self.client.login(username="User_1", password="pass") 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/tests/test_webhooks.py b/helpdesk/tests/test_webhooks.py index b885e19b..e10eaab9 100644 --- a/helpdesk/tests/test_webhooks.py +++ b/helpdesk/tests/test_webhooks.py @@ -1,9 +1,7 @@ from django.contrib.auth.models import User from helpdesk.models import Queue, CustomField, TicketCustomFieldValue, Ticket from helpdesk.serializers import TicketSerializer -from rest_framework.status import ( - HTTP_201_CREATED -) +from rest_framework.status import HTTP_201_CREATED from rest_framework.test import APITestCase import json import os @@ -15,42 +13,53 @@ import http.server import threading from http import HTTPStatus + class WebhookRequestHandler(http.server.BaseHTTPRequestHandler): server: "WebhookServer" def do_POST(self): - content_length = int(self.headers['Content-Length']) + content_length = int(self.headers["Content-Length"]) body = self.rfile.read(content_length) - self.server.requests.append({ - 'path': self.path, - 'headers': self.headers, - 'body': body - }) - if self.path == '/new-ticket': - self.server.handled_new_ticket_requests.append(json.loads(body.decode('utf-8'))) - if self.path == '/new-ticket-1': - self.server.handled_new_ticket_requests_1.append(json.loads(body.decode('utf-8'))) - elif self.path == '/followup': - self.server.handled_follow_up_requests.append(json.loads(body.decode('utf-8'))) - elif self.path == '/followup-1': - self.server.handled_follow_up_requests_1.append(json.loads(body.decode('utf-8'))) + self.server.requests.append( + {"path": self.path, "headers": self.headers, "body": body} + ) + if self.path == "/new-ticket": + self.server.handled_new_ticket_requests.append( + json.loads(body.decode("utf-8")) + ) + if self.path == "/new-ticket-1": + self.server.handled_new_ticket_requests_1.append( + json.loads(body.decode("utf-8")) + ) + elif self.path == "/followup": + self.server.handled_follow_up_requests.append( + json.loads(body.decode("utf-8")) + ) + elif self.path == "/followup-1": + self.server.handled_follow_up_requests_1.append( + json.loads(body.decode("utf-8")) + ) self.send_response(HTTPStatus.OK) self.end_headers() def do_GET(self): - if not self.path == '/get-past-requests': + if not self.path == "/get-past-requests": self.send_response(HTTPStatus.NOT_FOUND) self.end_headers() return self.send_response(HTTPStatus.OK) - self.send_header('Content-type', 'application/json') + self.send_header("Content-type", "application/json") self.end_headers() - self.wfile.write(json.dumps({ - 'new_ticket_requests': self.server.handled_new_ticket_requests, - 'new_ticket_requests_1': self.server.handled_new_ticket_requests_1, - 'follow_up_requests': self.server.handled_follow_up_requests, - 'follow_up_requests_1': self.server.handled_follow_up_requests_1 - }).encode('utf-8')) + self.wfile.write( + json.dumps( + { + "new_ticket_requests": self.server.handled_new_ticket_requests, + "new_ticket_requests_1": self.server.handled_new_ticket_requests_1, + "follow_up_requests": self.server.handled_follow_up_requests, + "follow_up_requests_1": self.server.handled_follow_up_requests_1, + } + ).encode("utf-8") + ) class WebhookServer(http.server.HTTPServer): @@ -64,7 +73,9 @@ class WebhookServer(http.server.HTTPServer): def start(self): self.thread = threading.Thread(target=self.serve_forever) - self.thread.daemon = True # Set as a daemon so it will be killed once the main thread is dead + self.thread.daemon = ( + True # Set as a daemon so it will be killed once the main thread is dead + ) self.thread.start() def stop(self): @@ -77,12 +88,12 @@ class WebhookTest(APITestCase): @classmethod def setUpTestData(cls): cls.queue = Queue.objects.create( - title='Test Queue', - slug='test-queue', + title="Test Queue", + slug="test-queue", ) def setUp(self): - staff_user = User.objects.create_user(username='test', is_staff=True) + staff_user = User.objects.create_user(username="test", is_staff=True) CustomField( name="my_custom_field", data_type="varchar", @@ -91,82 +102,135 @@ class WebhookTest(APITestCase): self.client.force_authenticate(staff_user) def test_test_server(self): - server = WebhookServer(('localhost', 8123), WebhookRequestHandler) + server = WebhookServer(("localhost", 8123), WebhookRequestHandler) server.start() - requests.post('http://localhost:8123/new-ticket', json={ - "foo": "bar"}) - handled_webhook_requests = requests.get('http://localhost:8123/get-past-requests').json() - self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["foo"], "bar") + requests.post("http://localhost:8123/new-ticket", json={"foo": "bar"}) + handled_webhook_requests = requests.get( + "http://localhost:8123/get-past-requests" + ).json() + self.assertEqual( + handled_webhook_requests["new_ticket_requests"][-1]["foo"], "bar" + ) server.stop() def test_create_ticket_and_followup_via_api(self): - server = WebhookServer(('localhost', 8124), WebhookRequestHandler) - os.environ['HELPDESK_NEW_TICKET_WEBHOOK_URLS'] = 'http://localhost:8124/new-ticket, http://localhost:8124/new-ticket-1' - os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8124/followup , http://localhost:8124/followup-1' + server = WebhookServer(("localhost", 8124), WebhookRequestHandler) + os.environ["HELPDESK_NEW_TICKET_WEBHOOK_URLS"] = ( + "http://localhost:8124/new-ticket, http://localhost:8124/new-ticket-1" + ) + os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = ( + "http://localhost:8124/followup , http://localhost:8124/followup-1" + ) server.start() - response = self.client.post('/api/tickets/', { - 'queue': self.queue.id, - 'title': 'Test title', - 'description': 'Test description\nMulti lines', - 'submitter_email': 'test@mail.com', - 'priority': 4, - 'custom_my_custom_field': 'custom value', - }) + response = self.client.post( + "/api/tickets/", + { + "queue": self.queue.id, + "title": "Test title", + "description": "Test description\nMulti lines", + "submitter_email": "test@mail.com", + "priority": 4, + "custom_my_custom_field": "custom value", + }, + ) self.assertEqual(CustomField.objects.all().first().name, "my_custom_field") - self.assertEqual(TicketCustomFieldValue.objects.get(ticket=response.data['id']).value, 'custom value') + self.assertEqual( + TicketCustomFieldValue.objects.get(ticket=response.data["id"]).value, + "custom value", + ) self.assertEqual(response.status_code, HTTP_201_CREATED) - handled_webhook_requests = requests.get('http://localhost:8124/get-past-requests') + handled_webhook_requests = requests.get( + "http://localhost:8124/get-past-requests" + ) handled_webhook_requests = handled_webhook_requests.json() - self.assertTrue(len(handled_webhook_requests['new_ticket_requests']) == 1) - self.assertTrue(len(handled_webhook_requests['new_ticket_requests_1']) == 1) - self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 0) - self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["title"], "Test title") - self.assertEqual(handled_webhook_requests['new_ticket_requests_1'][-1]["ticket"]["title"], "Test title") - self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["description"], "Test description\nMulti lines") - ticket = Ticket.objects.get(id=handled_webhook_requests["new_ticket_requests"][-1]["ticket"]["id"]) + self.assertTrue(len(handled_webhook_requests["new_ticket_requests"]) == 1) + self.assertTrue(len(handled_webhook_requests["new_ticket_requests_1"]) == 1) + self.assertEqual(len(handled_webhook_requests["follow_up_requests"]), 0) + self.assertEqual( + handled_webhook_requests["new_ticket_requests"][-1]["ticket"]["title"], + "Test title", + ) + self.assertEqual( + handled_webhook_requests["new_ticket_requests_1"][-1]["ticket"]["title"], + "Test title", + ) + self.assertEqual( + handled_webhook_requests["new_ticket_requests"][-1]["ticket"][ + "description" + ], + "Test description\nMulti lines", + ) + ticket = Ticket.objects.get( + id=handled_webhook_requests["new_ticket_requests"][-1]["ticket"]["id"] + ) ticket.set_custom_field_values() serializer = TicketSerializer(ticket) self.assertEqual( list(sorted(serializer.fields.keys())), - ['assigned_to', - 'attachment', - 'custom_my_custom_field', - 'description', - 'due_date', - 'followup_set', - 'id', - 'merged_to', - 'on_hold', - 'priority', - 'queue', - 'resolution', - 'status', - 'submitter_email', - 'title'] + [ + "assigned_to", + "attachment", + "custom_my_custom_field", + "description", + "due_date", + "followup_set", + "id", + "merged_to", + "on_hold", + "priority", + "queue", + "resolution", + "status", + "submitter_email", + "title", + ], + ) + self.assertEqual( + serializer.data, + handled_webhook_requests["new_ticket_requests"][-1]["ticket"], + ) + response = self.client.post( + "/api/followups/", + { + "ticket": handled_webhook_requests["new_ticket_requests"][-1]["ticket"][ + "id" + ], + "comment": "Test comment", + }, ) - self.assertEqual(serializer.data, handled_webhook_requests["new_ticket_requests"][-1]["ticket"]) - response = self.client.post('/api/followups/', { - 'ticket': handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["id"], - "comment": "Test comment", - }) self.assertEqual(response.status_code, HTTP_201_CREATED) - handled_webhook_requests = requests.get('http://localhost:8124/get-past-requests') + handled_webhook_requests = requests.get( + "http://localhost:8124/get-past-requests" + ) handled_webhook_requests = handled_webhook_requests.json() - self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 1) - self.assertEqual(len(handled_webhook_requests['follow_up_requests_1']), 1) - self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["followup_set"][-1]["comment"], "Test comment") - self.assertEqual(handled_webhook_requests['follow_up_requests_1'][-1]["ticket"]["followup_set"][-1]["comment"], "Test comment") - + self.assertEqual(len(handled_webhook_requests["follow_up_requests"]), 1) + self.assertEqual(len(handled_webhook_requests["follow_up_requests_1"]), 1) + self.assertEqual( + handled_webhook_requests["follow_up_requests"][-1]["ticket"][ + "followup_set" + ][-1]["comment"], + "Test comment", + ) + self.assertEqual( + handled_webhook_requests["follow_up_requests_1"][-1]["ticket"][ + "followup_set" + ][-1]["comment"], + "Test comment", + ) + server.stop() def test_create_ticket_and_followup_via_email(self): from .. import email - server = WebhookServer(('localhost', 8125), WebhookRequestHandler) - os.environ['HELPDESK_NEW_TICKET_WEBHOOK_URLS'] = 'http://localhost:8125/new-ticket' - os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8125/followup' + server = WebhookServer(("localhost", 8125), WebhookRequestHandler) + os.environ["HELPDESK_NEW_TICKET_WEBHOOK_URLS"] = ( + "http://localhost:8125/new-ticket" + ) + os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = "http://localhost:8125/followup" server.start() + class MockMessage(dict): def __init__(self, **kwargs): self.__dict__.update(kwargs) @@ -175,13 +239,13 @@ class WebhookTest(APITestCase): return self.__dict__.get(key, default) payload = { - 'body': "hello", - 'full_body': "hello", - 'subject': "Test subject", - 'queue': self.queue, - 'sender_email': "user@example.com", - 'priority': "1", - 'files': [], + "body": "hello", + "full_body": "hello", + "subject": "Test subject", + "queue": self.queue, + "sender_email": "user@example.com", + "priority": "1", + "files": [], } message = { @@ -195,26 +259,29 @@ class WebhookTest(APITestCase): ticket_id=None, payload=payload, files=[], - logger=logging.getLogger('helpdesk'), + logger=logging.getLogger("helpdesk"), ) - handled_webhook_requests = requests.get('http://localhost:8125/get-past-requests') + handled_webhook_requests = requests.get( + "http://localhost:8125/get-past-requests" + ) handled_webhook_requests = handled_webhook_requests.json() - self.assertEqual(len(handled_webhook_requests['new_ticket_requests']), 1) - self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 0) + self.assertEqual(len(handled_webhook_requests["new_ticket_requests"]), 1) + self.assertEqual(len(handled_webhook_requests["follow_up_requests"]), 0) - ticket_id = handled_webhook_requests['new_ticket_requests'][-1]["ticket"]['id'] + ticket_id = handled_webhook_requests["new_ticket_requests"][-1]["ticket"]["id"] from .. import models + ticket = models.Ticket.objects.get(id=ticket_id) payload = { - 'body': "hello", - 'full_body': "hello", - 'subject': f"[test-queue-{ticket_id}] Test subject", - 'queue': self.queue, - 'sender_email': "user@example.com", - 'priority': "1", - 'files': [], + "body": "hello", + "full_body": "hello", + "subject": f"[test-queue-{ticket_id}] Test subject", + "queue": self.queue, + "sender_email": "user@example.com", + "priority": "1", + "files": [], } message = { @@ -228,12 +295,22 @@ class WebhookTest(APITestCase): ticket_id=ticket_id, payload=payload, files=[], - logger=logging.getLogger('helpdesk'), + logger=logging.getLogger("helpdesk"), + ) + handled_webhook_requests = requests.get( + "http://localhost:8125/get-past-requests" ) - handled_webhook_requests = requests.get('http://localhost:8125/get-past-requests') handled_webhook_requests = handled_webhook_requests.json() - self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 1) - self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["followup_set"][-1]["comment"], "hello") - self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["id"], ticket_id) + self.assertEqual(len(handled_webhook_requests["follow_up_requests"]), 1) + self.assertEqual( + handled_webhook_requests["follow_up_requests"][-1]["ticket"][ + "followup_set" + ][-1]["comment"], + "hello", + ) + self.assertEqual( + handled_webhook_requests["follow_up_requests"][-1]["ticket"]["id"], + ticket_id, + ) server.stop() diff --git a/helpdesk/tests/urls.py b/helpdesk/tests/urls.py index e07fd6c8..5e5c85d1 100644 --- a/helpdesk/tests/urls.py +++ b/helpdesk/tests/urls.py @@ -3,6 +3,6 @@ from django.urls import include, path urlpatterns = [ - path('', include('helpdesk.urls', namespace='helpdesk')), - path('admin/', admin.site.urls), + path("", include("helpdesk.urls", namespace="helpdesk")), + path("admin/", admin.site.urls), ] diff --git a/helpdesk/tests/utils.py b/helpdesk/tests/utils.py index 51cf3c2a..159168d5 100644 --- a/helpdesk/tests/utils.py +++ b/helpdesk/tests/utils.py @@ -1,6 +1,5 @@ """UItility functions facilitate making unit testing easier and less brittle.""" - from PIL import Image import email from email import encoders @@ -31,8 +30,8 @@ def strip_accents(text): :returns: The processed String. :rtype: String. """ - text = unicodedata.normalize('NFD', text) - text = text.encode('ascii', 'ignore') + text = unicodedata.normalize("NFD", text) + text = text.encode("ascii", "ignore") text = text.decode("utf-8") return str(text) @@ -48,12 +47,12 @@ def text_to_id(text): :rtype: String. """ text = strip_accents(text.lower()) - text = re.sub('[ ]+', '_', text) - text = re.sub('[^0-9a-zA-Z_-]', '', text) + text = re.sub("[ ]+", "_", text) + text = re.sub("[^0-9a-zA-Z_-]", "", text) return text -def get_random_string(length: int=16) -> str: +def get_random_string(length: int = 16) -> str: return "".join( [random.choice(string.ascii_letters + string.digits) for _ in range(length)] ) @@ -62,27 +61,27 @@ def get_random_string(length: int=16) -> str: def generate_random_image(image_format, array_dims): """ Creates an image from a random array. - + :param image_format: An image format (PNG or JPEG). :param array_dims: A tuple with array dimensions. - + :returns: A byte string with encoded image :rtype: bytes """ - image_bytes = randint(low=0, high=255, size=array_dims, dtype='uint8') + image_bytes = randint(low=0, high=255, size=array_dims, dtype="uint8") io = BytesIO() image_pil = Image.fromarray(image_bytes) image_pil.save(io, image_format, subsampling=0, quality=100) return io.getvalue() -def get_random_image(image_format: str="PNG", size: int=5): +def get_random_image(image_format: str = "PNG", size: int = 5): """ Returns a random image. - + Args: image_format: An image format (PNG or JPEG). - + Returns: A string with encoded image """ @@ -92,120 +91,186 @@ def get_random_image(image_format: str="PNG", size: int=5): def get_fake(provider: str, locale: str = "en_US", min_length: int = 5) -> Any: """ Generates a random string, float, integer etc based on provider - Provider can be "text', 'sentence', "word" - e.g. `get_fake('name')` ==> 'Buzz Aldrin' + Provider can be "text', 'sentence', "word" + e.g. `get_fake('name')` ==> 'Buzz Aldrin' """ - string = factory.Faker(provider).evaluate({}, None, {'locale': locale,}) + string = factory.Faker(provider).evaluate( + {}, + None, + { + "locale": locale, + }, + ) while len(string) < min_length: - string += factory.Faker(provider).evaluate({}, None, {'locale': locale,}) + string += factory.Faker(provider).evaluate( + {}, + None, + { + "locale": locale, + }, + ) return string def get_fake_html(locale: str = "en_US", wrap_in_body_tag=True) -> Any: """ Generates a random string, float, integer etc based on provider - Provider can be "text', 'sentence', - e.g. `get_fake('name')` ==> 'Buzz Aldrin' + Provider can be "text', 'sentence', + e.g. `get_fake('name')` ==> 'Buzz Aldrin' """ - html = factory.Faker("sentence").evaluate({}, None, {'locale': locale,}) - for _ in range(0,4): - html += "
  • " + factory.Faker("sentence").evaluate({}, None, {'locale': locale,}) + "
  • " - for _ in range(0,4): - html += "

    " + factory.Faker("text").evaluate({}, None, {'locale': locale,}) + html = factory.Faker("sentence").evaluate( + {}, + None, + { + "locale": locale, + }, + ) + for _ in range(0, 4): + html += ( + "

  • " + + factory.Faker("sentence").evaluate( + {}, + None, + { + "locale": locale, + }, + ) + + "
  • " + ) + for _ in range(0, 4): + html += "

    " + factory.Faker("text").evaluate( + {}, + None, + { + "locale": locale, + }, + ) return f"{html}" if wrap_in_body_tag else html + def generate_email_address( - locale: str="en_US", - use_short_email: bool=False, - real_name_format: Optional[str]="{last_name}, {first_name}", - last_name_override: Optional[str]=None) -> Tuple[str, str, str, str]: - ''' + locale: str = "en_US", + use_short_email: bool = False, + real_name_format: Optional[str] = "{last_name}, {first_name}", + last_name_override: Optional[str] = None, +) -> Tuple[str, str, str, str]: + """ Generate an RFC 2822 email address - + :param locale: change this to generate locale specific names :param use_short_email: defaults to false. If true then does not include real name in email address :param real_name_format: pass a different format if different than "{last_name}, {first_name}" :param last_name_override: override the fake name if you want some special characters in the last name :returns , , , Message: + +def generate_file_mime_part( + locale: str = "en_US", filename: str = None, content: str = None +) -> Message: """ - + :param locale: change this to generate locale specific file name and attachment content :param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated :param content: pass a string value if you want have specific content otherwise a random string will be generated """ - part = MIMEBase('application', 'octet-stream') - part.set_payload(get_fake("text", locale=locale, min_length=1024) if content is None else content) + part = MIMEBase("application", "octet-stream") + part.set_payload( + get_fake("text", locale=locale, min_length=1024) if content is None else content + ) encoders.encode_base64(part) if not filename: filename = get_fake("word", locale=locale, min_length=8) + ".txt" - part.add_header('Content-Disposition', "attachment; filename=%s" % filename) + part.add_header("Content-Disposition", "attachment; filename=%s" % filename) return part -def generate_executable_mime_part(locale: str="en_US",filename: str = None, content: str = None) -> Message: + +def generate_executable_mime_part( + locale: str = "en_US", filename: str = None, content: str = None +) -> Message: """ - + :param locale: change this to generate locale specific file name and attachment content :param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated :param content: pass a string value if you want have specific content otherwise a random string will be generated """ - part = MIMEBase('application', 'vnd.microsoft.portable-executable') - part.set_payload(get_fake("text", locale=locale, min_length=1024) if content is None else content) + part = MIMEBase("application", "vnd.microsoft.portable-executable") + part.set_payload( + get_fake("text", locale=locale, min_length=1024) if content is None else content + ) encoders.encode_base64(part) if not filename: filename = get_fake("word", locale=locale, min_length=8) + ".exe" - part.add_header('Content-Disposition', "attachment; filename=%s" % filename) + part.add_header("Content-Disposition", "attachment; filename=%s" % filename) return part -def generate_image_mime_part(locale: str="en_US",imagename: str = None, disposition_primary_type: str = "attachment") -> Message: + +def generate_image_mime_part( + locale: str = "en_US", + imagename: str = None, + disposition_primary_type: str = "attachment", +) -> Message: """ - + :param locale: change this to generate locale specific file name and attachment content :param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated """ part = MIMEImage(generate_random_image(image_format="JPEG", array_dims=(200, 200))) - #part.set_payload(get_fake("text", locale=locale, min_length=1024)) + # part.set_payload(get_fake("text", locale=locale, min_length=1024)) encoders.encode_base64(part) if not imagename: imagename = get_fake("word", locale=locale, min_length=8) + ".jpg" - part.add_header('Content-Disposition', disposition_primary_type + "; filename= %s" % imagename) + part.add_header( + "Content-Disposition", disposition_primary_type + "; filename= %s" % imagename + ) return part -def generate_email_list(address_cnt: int = 3, - locale: str="en_US", - use_short_email: bool=False - ) -> str: + +def generate_email_list( + address_cnt: int = 3, locale: str = "en_US", use_short_email: bool = False +) -> str: """ Generates a list of email addresses formatted for email headers on a Mime part - + :param address_cnt: the number of email addresses to string together :param locale: change this to generate locale specific "real names" and subject :param use_short_email: produces a email address without "real name" if True """ - email_address_list = [generate_email_address(locale, use_short_email=use_short_email)[0] for _ in range(0, address_cnt)] + email_address_list = [ + generate_email_address(locale, use_short_email=use_short_email)[0] + for _ in range(0, address_cnt) + ] return ",".join(email_address_list) -def add_simple_email_headers(message: Message, locale: str="en_US", - use_short_email: bool=False - ) -> typing.Tuple[typing.Tuple[str, str], typing.Tuple[str, str]]: + +def add_simple_email_headers( + message: Message, locale: str = "en_US", use_short_email: bool = False +) -> typing.Tuple[typing.Tuple[str, str], typing.Tuple[str, str]]: """ Adds the key email headers to a Mime part - + :param message: the Mime part to add headers to :param locale: change this to generate locale specific "real names" and subject :param use_short_email: produces a "To" or "From" that is only the email address if True @@ -213,18 +278,20 @@ def add_simple_email_headers(message: Message, locale: str="en_US", """ to_meta = generate_email_address(locale, use_short_email=use_short_email) from_meta = generate_email_address(locale, use_short_email=use_short_email) - - message['Subject'] = get_fake("sentence", locale=locale) - message['From'] = from_meta[0] - message['To'] = to_meta[0] + + message["Subject"] = get_fake("sentence", locale=locale) + message["From"] = from_meta[0] + message["To"] = to_meta[0] return from_meta, to_meta -def generate_mime_part(locale: str="en_US", - part_type: str="plain", - ) -> typing.Optional[Message]: + +def generate_mime_part( + locale: str = "en_US", + part_type: str = "plain", +) -> typing.Optional[Message]: """ Generates amime part of the sepecified type - + :param locale: change this to generate locale specific strings :param text_type: options are plain, html, image (attachment), file (attachment) """ @@ -244,43 +311,53 @@ def generate_mime_part(locale: str="en_US", raise Exception("Mime part not implemented: " + part_type) return msg -def generate_multipart_email(locale: str="en_US", - type_list: typing.List[str]=["plain", "html", "image"], - sub_type: str = None, - use_short_email: bool=False - ) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]: + +def generate_multipart_email( + locale: str = "en_US", + type_list: typing.List[str] = ["plain", "html", "image"], + sub_type: str = None, + use_short_email: bool = False, +) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]: """ Generates an email including headers with the defined multiparts - + :param locale: :param type_list: options are plain, html, image (attachment), file (attachment), and executable (.exe attachment) :param sub_type: multipart sub type that defaults to "mixed" if not specified :param use_short_email: produces a "To" or "From" that is only the email address if True - """ + """ msg = MIMEMultipart(sub_type) if sub_type else MIMEMultipart() for part_type in type_list: msg.attach(generate_mime_part(locale=locale, part_type=part_type)) - from_meta, to_meta = add_simple_email_headers(msg, locale=locale, use_short_email=use_short_email) + from_meta, to_meta = add_simple_email_headers( + msg, locale=locale, use_short_email=use_short_email + ) return msg, from_meta, to_meta -def generate_text_email(locale: str="en_US", - use_short_email: bool=False - ) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]: + +def generate_text_email( + locale: str = "en_US", use_short_email: bool = False +) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]: """ Generates an email including headers """ body = get_fake("text", locale=locale, min_length=1024) msg = MIMEText(body) - from_meta, to_meta = add_simple_email_headers(msg, locale=locale, use_short_email=use_short_email) + from_meta, to_meta = add_simple_email_headers( + msg, locale=locale, use_short_email=use_short_email + ) return msg, from_meta, to_meta -def generate_html_email(locale: str="en_US", - use_short_email: bool=False - ) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]: + +def generate_html_email( + locale: str = "en_US", use_short_email: bool = False +) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]: """ Generates an email including headers """ body = get_fake_html(locale=locale) msg = MIMEText(body) - from_meta, to_meta = add_simple_email_headers(msg, locale=locale, use_short_email=use_short_email) + from_meta, to_meta = add_simple_email_headers( + msg, locale=locale, use_short_email=use_short_email + ) return msg, from_meta, to_meta diff --git a/helpdesk/update_ticket.py b/helpdesk/update_ticket.py index 51ac7d19..50614a9f 100644 --- a/helpdesk/update_ticket.py +++ b/helpdesk/update_ticket.py @@ -20,15 +20,15 @@ from helpdesk.signals import update_ticket_done User = get_user_model() -def add_staff_subscription( - user: User, - ticket: Ticket -) -> None: + +def add_staff_subscription(user: User, ticket: Ticket) -> None: """Auto subscribe the staff member if that's what the settigs say and the user is authenticated and a staff member""" - if helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE \ - and user.is_authenticated \ - and return_ticketccstring_and_show_subscribe(user, ticket)[1]: + if ( + helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE + and user.is_authenticated + and return_ticketccstring_and_show_subscribe(user, ticket)[1] + ): subscribe_to_ticket_updates(ticket, user) @@ -45,7 +45,7 @@ def return_ticketccstring_and_show_subscribe(user, ticket): strings_to_check.append(username) strings_to_check.append(useremail) - ticketcc_string = '' + ticketcc_string = "" all_ticketcc = ticket.ticketcc_set.all() counter_all_ticketcc = len(all_ticketcc) - 1 show_subscribe = True @@ -53,7 +53,7 @@ def return_ticketccstring_and_show_subscribe(user, ticket): ticketcc_this_entry = str(ticketcc.display) ticketcc_string += ticketcc_this_entry if i < counter_all_ticketcc: - ticketcc_string += ', ' + ticketcc_string += ", " if strings_to_check.__contains__(ticketcc_this_entry.upper()): show_subscribe = False @@ -64,18 +64,19 @@ def return_ticketccstring_and_show_subscribe(user, ticket): submitter_email = ticket.submitter_email.upper() strings_to_check.append(submitter_email) strings_to_check.append(assignedto_username) - if strings_to_check.__contains__(username) or strings_to_check.__contains__(useremail): + if strings_to_check.__contains__(username) or strings_to_check.__contains__( + useremail + ): show_subscribe = False return ticketcc_string, show_subscribe -def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, can_update=False): - +def subscribe_to_ticket_updates( + ticket, user=None, email=None, can_view=True, can_update=False +): 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: @@ -83,21 +84,19 @@ def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, ca if user is None and len(email) < 5: raise ValidationError( - _('When you add somebody on Cc, you must provide either a User or a valid email. Email: %s' % email) + _( + "When you add somebody on Cc, you must provide either a User or a valid email. Email: %s" + % email + ) ) return ticket.ticketcc_set.create( - user=user, - email=email, - can_view=can_view, - can_update=can_update + user=user, email=email, can_view=can_view, can_update=can_update ) def get_and_set_ticket_status( - new_status: int, - ticket: Ticket, - follow_up: FollowUp + new_status: int, ticket: Ticket, follow_up: FollowUp ) -> typing.Tuple[str, int]: """Performs comparision on previous status to new status, updating the title as required. @@ -112,15 +111,15 @@ def get_and_set_ticket_status( ticket.save() follow_up.new_status = new_status if follow_up.title: - follow_up.title += ' and %s' % ticket.get_status_display() + follow_up.title += " and %s" % ticket.get_status_display() else: - follow_up.title = '%s' % ticket.get_status_display() + follow_up.title = "%s" % ticket.get_status_display() if not follow_up.title: if follow_up.comment: - follow_up.title = _('Comment') + follow_up.title = _("Comment") else: - follow_up.title = _('Updated') + follow_up.title = _("Updated") follow_up.save() return old_status_str, old_status @@ -132,80 +131,76 @@ def update_messages_sent_to_by_public_and_status( follow_up: FollowUp, context: str, messages_sent_to: typing.Set[str], - files: typing.List[typing.Tuple[str, str]] + files: typing.List[typing.Tuple[str, str]], ) -> Ticket: """Sets the status of the ticket""" if public and ( - follow_up.comment or ( - follow_up.new_status in ( - Ticket.RESOLVED_STATUS, - Ticket.CLOSED_STATUS - ) - ) + follow_up.comment + or (follow_up.new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS)) ): if follow_up.new_status == Ticket.RESOLVED_STATUS: - template = 'resolved_' + template = "resolved_" elif follow_up.new_status == Ticket.CLOSED_STATUS: - template = 'closed_' + template = "closed_" else: - template = 'updated_' + template = "updated_" roles = { - 'submitter': (template + 'submitter', context), - 'ticket_cc': (template + 'cc', context), + "submitter": (template + "submitter", context), + "ticket_cc": (template + "cc", context), } - if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change: - roles['assigned_to'] = (template + 'cc', context) + 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 + roles, dont_send_to=messages_sent_to, fail_silently=True, files=files ) ) return ticket def get_template_staff_and_template_cc( - reassigned, follow_up: FollowUp + reassigned, follow_up: FollowUp ) -> typing.Tuple[str, str]: if reassigned: - template_staff = 'assigned_owner' + template_staff = "assigned_owner" elif follow_up.new_status == Ticket.RESOLVED_STATUS: - template_staff = 'resolved_owner' + template_staff = "resolved_owner" elif follow_up.new_status == Ticket.CLOSED_STATUS: - template_staff = 'closed_owner' + template_staff = "closed_owner" else: - template_staff = 'updated_owner' + template_staff = "updated_owner" if reassigned: - template_cc = 'assigned_cc' + template_cc = "assigned_cc" elif follow_up.new_status == Ticket.RESOLVED_STATUS: - template_cc = 'resolved_cc' + template_cc = "resolved_cc" elif follow_up.new_status == Ticket.CLOSED_STATUS: - template_cc = 'closed_cc' + template_cc = "closed_cc" else: - template_cc = 'updated_cc' + template_cc = "updated_cc" return template_staff, template_cc def update_ticket( - user, - ticket, - title=None, - comment="", - files=None, - public=False, - owner=-1, - priority=-1, - queue=-1, - new_status=None, - time_spent=None, - due_date=None, - new_checklists=None, - message_id=None, - customfields_form=None, + user, + ticket, + title=None, + comment="", + files=None, + public=False, + owner=-1, + priority=-1, + queue=-1, + new_status=None, + time_spent=None, + due_date=None, + new_checklists=None, + message_id=None, + customfields_form=None, ): # We need to allow the 'ticket' and 'queue' contexts to be applied to the # comment. @@ -222,25 +217,31 @@ def update_ticket( new_checklists = {} from django.template import engines - template_func = engines['django'].from_string + + template_func = engines["django"].from_string # 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', '{% verbatim %}{%' - ).replace( - 'X-HELPDESK-COMMENT-ENDVERBATIM', '%}{% endverbatim %}' + comment = comment.replace("{%", "X-HELPDESK-COMMENT-VERBATIM").replace( + "%}", "X-HELPDESK-COMMENT-ENDVERBATIM" ) + comment = comment.replace( + "X-HELPDESK-COMMENT-VERBATIM", "{% verbatim %}{%" + ).replace("X-HELPDESK-COMMENT-ENDVERBATIM", "%}{% endverbatim %}") # render the neutralized template comment = template_func(comment).render(context) if owner == -1 and ticket.assigned_to: owner = ticket.assigned_to.id - f = FollowUp(ticket=ticket, date=timezone.now(), comment=comment, - time_spent=time_spent, message_id=message_id, title=title) + f = FollowUp( + ticket=ticket, + date=timezone.now(), + comment=comment, + time_spent=time_spent, + message_id=message_id, + title=title, + ) if is_helpdesk_staff(user): f.user = user @@ -251,16 +252,19 @@ def update_ticket( old_owner = ticket.assigned_to if owner != -1: - if owner != 0 and ((ticket.assigned_to and owner != ticket.assigned_to.id) or not ticket.assigned_to): + if owner != 0 and ( + (ticket.assigned_to and owner != ticket.assigned_to.id) + or not ticket.assigned_to + ): new_user = User.objects.get(id=owner) - f.title = _('Assigned to %(username)s') % { - 'username': new_user.get_username(), + f.title = _("Assigned to %(username)s") % { + "username": new_user.get_username(), } ticket.assigned_to = new_user reassigned = True # user changed owner to 'unassign' elif owner == 0 and ticket.assigned_to is not None: - f.title = _('Unassigned') + f.title = _("Unassigned") ticket.assigned_to = None old_status_str, old_status = get_and_set_ticket_status(new_status, ticket, f) @@ -269,7 +273,7 @@ def update_ticket( if title and title != ticket.title: c = f.ticketchange_set.create( - field=_('Title'), + field=_("Title"), old_value=ticket.title, new_value=title, ) @@ -277,21 +281,21 @@ def update_ticket( if new_status != old_status: c = f.ticketchange_set.create( - field=_('Status'), + field=_("Status"), old_value=old_status_str, new_value=ticket.get_status_display(), ) if ticket.assigned_to != old_owner: c = f.ticketchange_set.create( - field=_('Owner'), + field=_("Owner"), old_value=old_owner, new_value=ticket.assigned_to, ) if priority != ticket.priority: c = f.ticketchange_set.create( - field=_('Priority'), + field=_("Priority"), old_value=ticket.priority, new_value=priority, ) @@ -299,7 +303,7 @@ def update_ticket( if queue != ticket.queue.id: c = f.ticketchange_set.create( - field=_('Queue'), + field=_("Queue"), old_value=ticket.queue.id, new_value=queue, ) @@ -307,16 +311,16 @@ def update_ticket( if due_date != ticket.due_date: c = f.ticketchange_set.create( - field=_('Due on'), + field=_("Due on"), old_value=ticket.due_date, new_value=due_date, ) ticket.due_date = due_date - + # save custom fields and ticket changes if customfields_form and customfields_form.is_valid(): customfields_form.save(followup=f) - + for checklist in ticket.checklists.all(): if checklist.id not in new_checklists: continue @@ -327,24 +331,22 @@ def update_ticket( # Add completion if it was not done yet if not task.completion_date and task.id in new_completed_tasks: task.completion_date = timezone.now() - changed = 'completed' + changed = "completed" # Remove it if it was done before elif task.completion_date and task.id not in new_completed_tasks: task.completion_date = None - changed = 'uncompleted' + changed = "uncompleted" # Save and add ticket change if task state has changed if changed: - task.save(update_fields=['completion_date']) + task.save(update_fields=["completion_date"]) f.ticketchange_set.create( - field=f'[{checklist.name}] {task.description}', - old_value=_('To do') if changed == 'completed' else _('Completed'), - new_value=_('Completed') if changed == 'completed' else _('To do'), + field=f"[{checklist.name}] {task.description}", + old_value=_("To do") if changed == "completed" else _("Completed"), + new_value=_("Completed") if changed == "completed" else _("To do"), ) - if new_status in ( - Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS - ) and ( + if new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS) and ( new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None ): ticket.resolution = comment @@ -363,32 +365,34 @@ def update_ticket( except AttributeError: pass ticket = update_messages_sent_to_by_public_and_status( - public, - ticket, - f, - context, - messages_sent_to, - files + public, ticket, f, context, messages_sent_to, files ) template_staff, template_cc = get_template_staff_and_template_cc(reassigned, f) if ticket.assigned_to and ( ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change - or (reassigned and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign) + or ( + reassigned + and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign + ) ): - messages_sent_to.update(ticket.send( - {'assigned_to': (template_staff, context)}, + messages_sent_to.update( + ticket.send( + {"assigned_to": (template_staff, context)}, + dont_send_to=messages_sent_to, + fail_silently=True, + files=files, + ) + ) + + messages_sent_to.update( + ticket.send( + {"ticket_cc": (template_cc, context)}, dont_send_to=messages_sent_to, fail_silently=True, files=files, - )) - - messages_sent_to.update(ticket.send( - {'ticket_cc': (template_cc, context)}, - dont_send_to=messages_sent_to, - fail_silently=True, - files=files, - )) + ) + ) ticket.save() # emit signal with followup when the ticket update is done @@ -398,4 +402,3 @@ def update_ticket( # auto subscribe user if enabled add_staff_subscription(user, ticket) return f - diff --git a/helpdesk/urls.py b/helpdesk/urls.py index 6cc7eb99..ed50f2c7 100644 --- a/helpdesk/urls.py +++ b/helpdesk/urls.py @@ -14,7 +14,13 @@ from django.views.generic import TemplateView from helpdesk import settings as helpdesk_settings from helpdesk.decorators import helpdesk_staff_member_required, protect_view from helpdesk.views import feeds, login, public, staff -from helpdesk.views.api import CreateUserView, FollowUpAttachmentViewSet, FollowUpViewSet, TicketViewSet, UserTicketViewSet +from helpdesk.views.api import ( + CreateUserView, + FollowUpAttachmentViewSet, + FollowUpViewSet, + TicketViewSet, + UserTicketViewSet, +) from rest_framework.routers import DefaultRouter @@ -63,16 +69,12 @@ urlpatterns = [ name="followup_delete", ), path("tickets//edit/", staff.edit_ticket, name="edit"), - path("tickets//update/", - staff.update_ticket_view, name="update"), - path("tickets//delete/", - staff.delete_ticket, name="delete"), + path("tickets//update/", staff.update_ticket_view, 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, @@ -106,35 +108,33 @@ urlpatterns = [ path( "tickets//checklists//", staff.edit_ticket_checklist, - name="edit_ticket_checklist" + name="edit_ticket_checklist", ), path( "tickets//checklists//delete/", staff.delete_ticket_checklist, - name="delete_ticket_checklist" + name="delete_ticket_checklist", ), 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"), path("checklist-templates/", staff.checklist_templates, name="checklist_templates"), path( "checklist-templates//", staff.checklist_templates, - name="edit_checklist_template" + name="edit_checklist_template", ), path( "checklist-templates//delete/", staff.delete_checklist_template, - name="delete_checklist_template" + name="delete_checklist_template", ), re_path( r"^datatables_ticket_list/(?P{})$".format(base64_pattern), @@ -164,7 +164,11 @@ if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET: urlpatterns += [ path("", protect_view(public.Homepage.as_view()), name="home"), - path("tickets/my-tickets/", protect_view(public.MyTickets.as_view()), name="my-tickets"), + path( + "tickets/my-tickets/", + protect_view(public.MyTickets.as_view()), + name="my-tickets", + ), path("tickets/submit/", public.create_ticket, name="submit"), path( "tickets/submit_iframe/", @@ -177,8 +181,7 @@ urlpatterns += [ name="success_iframe", ), path("view/", protect_view(public.ViewTicket.as_view()), name="public_view"), - path("change_language/", public.change_language, - name="public_change_language"), + path("change_language/", public.change_language, name="public_change_language"), ] urlpatterns += [ @@ -214,8 +217,9 @@ router = DefaultRouter() router.register(r"tickets", TicketViewSet, basename="ticket") router.register(r"user_tickets", UserTicketViewSet, basename="user_tickets") 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))] @@ -249,8 +253,7 @@ 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"), re_path(r"^kb/(?P\d+)/vote/(?Pup|down)/$", kb.vote, name="kb_vote"), re_path( r"^kb_iframe/(?P[A-Za-z0-9_-]+)/$", @@ -268,8 +271,7 @@ 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 e21fd064..9247c259 100644 --- a/helpdesk/user.py +++ b/helpdesk/user.py @@ -1,4 +1,3 @@ - from helpdesk import settings as helpdesk_settings from helpdesk.models import Queue, Ticket @@ -23,14 +22,13 @@ class HelpdeskUser: """ user = self.user all_queues = Queue.objects.all() - public_ids = [q.pk for q in - Queue.objects.filter(allow_public_submission=True)] - limit_queues_by_user = \ - helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION \ + public_ids = [q.pk for q in Queue.objects.filter(allow_public_submission=True)] + limit_queues_by_user = ( + helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION and not user.is_superuser + ) if limit_queues_by_user: - id_list = [q.pk for q in all_queues if user.has_perm( - q.permission_name)] + 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: @@ -56,8 +54,11 @@ class HelpdeskUser: return Ticket.objects.filter(queue__in=self.get_queues()) def has_full_access(self): - return self.user.is_superuser or self.user.is_staff \ + return ( + self.user.is_superuser + or self.user.is_staff or helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE + ) def can_access_queue(self, queue): """Check if a certain user can access a certain queue. @@ -71,18 +72,18 @@ class HelpdeskUser: else: return ( helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION - and - self.user.has_perm(queue.permission_name) + and self.user.has_perm(queue.permission_name) ) def can_access_ticket(self, ticket): """Check to see if the user has permission to access - a ticket. If not then deny access.""" + a ticket. If not then deny access.""" user = self.user if self.can_access_queue(ticket.queue): return True - elif self.has_full_access() or \ - (ticket.assigned_to and user.id == ticket.assigned_to.id): + elif self.has_full_access() or ( + ticket.assigned_to and user.id == ticket.assigned_to.id + ): return True else: return False @@ -90,4 +91,6 @@ class HelpdeskUser: def can_access_kbcategory(self, category): if category.public: return True - return self.has_full_access() or (category.queue and self.can_access_queue(category.queue)) + return self.has_full_access() or ( + category.queue and self.can_access_queue(category.queue) + ) diff --git a/helpdesk/validators.py b/helpdesk/validators.py index 0fc58703..3ea397bf 100644 --- a/helpdesk/validators.py +++ b/helpdesk/validators.py @@ -14,6 +14,7 @@ from helpdesk import settings as helpdesk_settings def validate_file_extension(value): from django.core.exceptions import ValidationError import os + ext = os.path.splitext(value.name)[1] # [0] returns path+filename # TODO: we might improve this with more thorough checks of file types # rather than just the extensions. @@ -24,7 +25,5 @@ def validate_file_extension(value): if ext.lower() not in helpdesk_settings.HELPDESK_VALID_EXTENSIONS: # 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: ') + ext.lower() - ) + if not (ext.lower() == "" or ext.lower() == "."): + raise ValidationError(_("Unsupported file extension: ") + ext.lower()) diff --git a/helpdesk/views/abstract_views.py b/helpdesk/views/abstract_views.py index f7e5eb62..b5227ffe 100644 --- a/helpdesk/views/abstract_views.py +++ b/helpdesk/views/abstract_views.py @@ -1,23 +1,28 @@ from helpdesk.models import CustomField, KBItem, Queue -class AbstractCreateTicketMixin(): +class AbstractCreateTicketMixin: def get_initial(self): 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 + 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'] + query_param_fields = ["submitter_email", "title", "body", "queue", "kbitem"] custom_fields = [ - "custom_%s" % f.name for f in CustomField.objects.filter(staff_only=False)] + "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, "")) @@ -27,13 +32,12 @@ class AbstractCreateTicketMixin(): def get_form_kwargs(self, *args, **kwargs): kwargs = super().get_form_kwargs(*args, **kwargs) kbitem = self.request.GET.get( - 'kbitem', - self.request.POST.get('kbitem', None), + "kbitem", + self.request.POST.get("kbitem", None), ) 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/api.py b/helpdesk/views/api.py index 8e2e9edd..05ea9123 100644 --- a/helpdesk/views/api.py +++ b/helpdesk/views/api.py @@ -1,6 +1,12 @@ from django.contrib.auth import get_user_model from helpdesk.models import FollowUp, FollowUpAttachment, Ticket -from helpdesk.serializers import FollowUpAttachmentSerializer, FollowUpSerializer, TicketSerializer, UserSerializer, PublicTicketListingSerializer +from helpdesk.serializers import ( + FollowUpAttachmentSerializer, + FollowUpSerializer, + TicketSerializer, + UserSerializer, + PublicTicketListingSerializer, +) from rest_framework import viewsets from rest_framework.mixins import CreateModelMixin from rest_framework.permissions import IsAdminUser, IsAuthenticated @@ -12,7 +18,7 @@ from helpdesk import settings as helpdesk_settings class ConservativePagination(PageNumberPagination): page_size = 25 - page_size_query_param = 'page_size' + page_size_query_param = "page_size" class UserTicketViewSet(viewsets.ReadOnlyModelViewSet): @@ -21,18 +27,20 @@ class UserTicketViewSet(viewsets.ReadOnlyModelViewSet): The view is paginated by default """ + serializer_class = PublicTicketListingSerializer pagination_class = ConservativePagination permission_classes = [IsAuthenticated] def get_queryset(self): - tickets = Ticket.objects.filter(submitter_email=self.request.user.email).order_by('-created') + tickets = Ticket.objects.filter( + submitter_email=self.request.user.email + ).order_by("-created") for ticket in tickets: ticket.set_custom_field_values() return tickets - class TicketViewSet(viewsets.ModelViewSet): """ A viewset that provides the standard actions to handle Ticket @@ -41,6 +49,7 @@ class TicketViewSet(viewsets.ModelViewSet): `/api/tickets/?status=Open,Resolved` will return all the tickets that are Open or Resolved. """ + queryset = Ticket.objects.all() serializer_class = TicketSerializer pagination_class = ConservativePagination @@ -50,17 +59,17 @@ class TicketViewSet(viewsets.ModelViewSet): tickets = Ticket.objects.all() # filter by status - status = self.request.query_params.get('status', None) + status = self.request.query_params.get("status", None) if status: - statuses = status.split(',') if status else [] - status_choices = helpdesk_settings.TICKET_STATUS_CHOICES - number_statuses = [] - for status in statuses: - for choice in status_choices: - if str(choice[0]) == status: - number_statuses.append(choice[0]) - if number_statuses: - tickets = tickets.filter(status__in=number_statuses) + statuses = status.split(",") if status else [] + status_choices = helpdesk_settings.TICKET_STATUS_CHOICES + number_statuses = [] + for status in statuses: + for choice in status_choices: + if str(choice[0]) == status: + number_statuses.append(choice[0]) + if number_statuses: + tickets = tickets.filter(status__in=number_statuses) for ticket in tickets: ticket.set_custom_field_values() diff --git a/helpdesk/views/feeds.py b/helpdesk/views/feeds.py index 0653a242..a78573be 100644 --- a/helpdesk/views/feeds.py +++ b/helpdesk/views/feeds.py @@ -25,8 +25,8 @@ for open_status in Ticket.OPEN_STATUSES: class OpenTicketsByUser(Feed): - title_template = 'helpdesk/rss/ticket_title.html' - description_template = 'helpdesk/rss/ticket_description.html' + title_template = "helpdesk/rss/ticket_title.html" + description_template = "helpdesk/rss/ticket_description.html" def get_object(self, request, user_name, queue_slug=None): user = get_object_or_404(User, username=user_name) @@ -35,54 +35,56 @@ class OpenTicketsByUser(Feed): else: queue = None - return {'user': user, 'queue': queue} + return {"user": user, "queue": queue} def title(self, obj): - if obj['queue']: + if obj["queue"]: return _("Helpdesk: Open Tickets in queue %(queue)s for %(username)s") % { - 'queue': obj['queue'].title, - 'username': obj['user'].get_username(), + "queue": obj["queue"].title, + "username": obj["user"].get_username(), } else: return _("Helpdesk: Open Tickets for %(username)s") % { - 'username': obj['user'].get_username(), + "username": obj["user"].get_username(), } def description(self, obj): - if obj['queue']: - return _("Open and Reopened Tickets in queue %(queue)s for %(username)s") % { - 'queue': obj['queue'].title, - 'username': obj['user'].get_username(), + if obj["queue"]: + return _( + "Open and Reopened Tickets in queue %(queue)s for %(username)s" + ) % { + "queue": obj["queue"].title, + "username": obj["user"].get_username(), } else: return _("Open and Reopened Tickets for %(username)s") % { - 'username': obj['user'].get_username(), + "username": obj["user"].get_username(), } def link(self, obj): - if obj['queue']: - return u'%s?assigned_to=%s&queue=%s' % ( - reverse('helpdesk:list'), - obj['user'].id, - obj['queue'].id, + if obj["queue"]: + return "%s?assigned_to=%s&queue=%s" % ( + reverse("helpdesk:list"), + obj["user"].id, + obj["queue"].id, ) else: - return u'%s?assigned_to=%s' % ( - reverse('helpdesk:list'), - obj['user'].id, + return "%s?assigned_to=%s" % ( + reverse("helpdesk:list"), + obj["user"].id, ) def items(self, obj): - if obj['queue']: - return Ticket.objects.filter( - assigned_to=obj['user'] - ).filter( - queue=obj['queue'] - ).filter(Q_OPEN_STATUSES) + if obj["queue"]: + return ( + Ticket.objects.filter(assigned_to=obj["user"]) + .filter(queue=obj["queue"]) + .filter(Q_OPEN_STATUSES) + ) else: - return Ticket.objects.filter( - assigned_to=obj['user'] - ).filter(Q_OPEN_STATUSES) + return Ticket.objects.filter(assigned_to=obj["user"]).filter( + Q_OPEN_STATUSES + ) def item_pubdate(self, item): return item.created @@ -91,21 +93,19 @@ class OpenTicketsByUser(Feed): if item.assigned_to: return item.assigned_to.get_username() else: - return _('Unassigned') + return _("Unassigned") class UnassignedTickets(Feed): - title_template = 'helpdesk/rss/ticket_title.html' - description_template = 'helpdesk/rss/ticket_description.html' + title_template = "helpdesk/rss/ticket_title.html" + description_template = "helpdesk/rss/ticket_description.html" - title = _('Helpdesk: Unassigned Tickets') - description = _('Unassigned Open and Reopened tickets') - link = '' # '%s?assigned_to=' % reverse('helpdesk:list') + title = _("Helpdesk: Unassigned Tickets") + description = _("Unassigned Open and Reopened tickets") + link = "" # '%s?assigned_to=' % reverse('helpdesk:list') def items(self, obj): - return Ticket.objects.filter( - assigned_to__isnull=True - ).filter(Q_OPEN_STATUSES) + return Ticket.objects.filter(assigned_to__isnull=True).filter(Q_OPEN_STATUSES) def item_pubdate(self, item): return item.created @@ -114,49 +114,48 @@ class UnassignedTickets(Feed): if item.assigned_to: return item.assigned_to.get_username() else: - return _('Unassigned') + return _("Unassigned") class RecentFollowUps(Feed): - title_template = 'helpdesk/rss/recent_activity_title.html' - description_template = 'helpdesk/rss/recent_activity_description.html' + title_template = "helpdesk/rss/recent_activity_title.html" + description_template = "helpdesk/rss/recent_activity_description.html" - title = _('Helpdesk: Recent Followups') + title = _("Helpdesk: Recent Followups") description = _( - 'Recent FollowUps, such as e-mail replies, comments, attachments and resolutions') - link = '/tickets/' # reverse('helpdesk:list') + "Recent FollowUps, such as e-mail replies, comments, attachments and resolutions" + ) + link = "/tickets/" # reverse('helpdesk:list') def items(self): - return FollowUp.objects.order_by('-date')[:20] + return FollowUp.objects.order_by("-date")[:20] class OpenTicketsByQueue(Feed): - title_template = 'helpdesk/rss/ticket_title.html' - description_template = 'helpdesk/rss/ticket_description.html' + title_template = "helpdesk/rss/ticket_title.html" + description_template = "helpdesk/rss/ticket_description.html" def get_object(self, request, queue_slug): return get_object_or_404(Queue, slug=queue_slug) def title(self, obj): - return _('Helpdesk: Open Tickets in queue %(queue)s') % { - 'queue': obj.title, + return _("Helpdesk: Open Tickets in queue %(queue)s") % { + "queue": obj.title, } def description(self, obj): - return _('Open and Reopened Tickets in queue %(queue)s') % { - 'queue': obj.title, + return _("Open and Reopened Tickets in queue %(queue)s") % { + "queue": obj.title, } def link(self, obj): - return '%s?queue=%s' % ( - reverse('helpdesk:list'), + return "%s?queue=%s" % ( + reverse("helpdesk:list"), obj.id, ) def items(self, obj): - return Ticket.objects.filter( - queue=obj - ).filter(Q_OPEN_STATUSES) + return Ticket.objects.filter(queue=obj).filter(Q_OPEN_STATUSES) def item_pubdate(self, item): return item.created @@ -165,4 +164,4 @@ class OpenTicketsByQueue(Feed): if item.assigned_to: return item.assigned_to.get_username() else: - return _('Unassigned') + return _("Unassigned") diff --git a/helpdesk/views/kb.py b/helpdesk/views/kb.py index 588878ce..f4d6fa67 100644 --- a/helpdesk/views/kb.py +++ b/helpdesk/views/kb.py @@ -18,10 +18,14 @@ from helpdesk.models import KBCategory, KBItem def index(request): huser = user.huser_from_request(request) # TODO: It'd be great to have a list of most popular items here. - return render(request, 'helpdesk/kb_index.html', { - 'kb_categories': huser.get_allowed_kb_categories(), - 'helpdesk_settings': helpdesk_settings, - }) + return render( + request, + "helpdesk/kb_index.html", + { + "kb_categories": huser.get_allowed_kb_categories(), + "helpdesk_settings": helpdesk_settings, + }, + ) def category(request, slug, iframe=False): @@ -29,29 +33,33 @@ def category(request, slug, iframe=False): if not user.huser_from_request(request).can_access_kbcategory(category): raise Http404 items = category.kbitem_set.filter(enabled=True) - selected_item = request.GET.get('kbitem', None) + selected_item = request.GET.get("kbitem", None) try: selected_item = int(selected_item) except TypeError: pass qparams = request.GET.copy() try: - del qparams['kbitem'] + del qparams["kbitem"] except KeyError: pass - template = 'helpdesk/kb_category.html' + template = "helpdesk/kb_category.html" if iframe: - template = 'helpdesk/kb_category_iframe.html' + template = "helpdesk/kb_category_iframe.html" staff = request.user.is_authenticated and request.user.is_staff - return render(request, template, { - 'category': category, - 'items': items, - 'selected_item': selected_item, - 'query_param_string': qparams.urlencode(), - 'helpdesk_settings': helpdesk_settings, - 'iframe': iframe, - 'staff': staff, - }) + return render( + request, + template, + { + "category": category, + "items": items, + "selected_item": selected_item, + "query_param_string": qparams.urlencode(), + "helpdesk_settings": helpdesk_settings, + "iframe": iframe, + "staff": staff, + }, + ) @xframe_options_exempt @@ -62,7 +70,7 @@ def category_iframe(request, slug): def vote(request, item, vote): item = get_object_or_404(KBItem, pk=item) if request.method == "POST": - if vote == 'up': + if vote == "up": if not item.voted_by.filter(pk=request.user.pk): item.votes += 1 item.voted_by.add(request.user.pk) @@ -70,7 +78,7 @@ def vote(request, item, vote): if item.downvoted_by.filter(pk=request.user.pk): item.votes -= 1 item.downvoted_by.remove(request.user.pk) - if vote == 'down': + if vote == "down": if not item.downvoted_by.filter(pk=request.user.pk): item.votes += 1 item.downvoted_by.add(request.user.pk) diff --git a/helpdesk/views/login.py b/helpdesk/views/login.py index dfc96a50..a0d67ef7 100644 --- a/helpdesk/views/login.py +++ b/helpdesk/views/login.py @@ -5,24 +5,22 @@ from django.shortcuts import resolve_url default_login_view = auth_views.LoginView.as_view( - template_name='helpdesk/registration/login.html') + template_name="helpdesk/registration/login.html" +) def login(request): login_url = settings.LOGIN_URL # Prevent redirect loop by checking that LOGIN_URL is not this view's name - condition = ( - login_url - and ( - login_url != resolve_url(request.resolver_match.view_name) - and (login_url != request.resolver_match.view_name) - ) + condition = login_url and ( + login_url != resolve_url(request.resolver_match.view_name) + and (login_url != request.resolver_match.view_name) ) if condition: - if 'next' in request.GET: - return_to = request.GET['next'] + if "next" in request.GET: + return_to = request.GET["next"] else: - return_to = resolve_url('helpdesk:home') + return_to = resolve_url("helpdesk:home") return redirect_to_login(return_to, login_url) else: return default_login_view(request) diff --git a/helpdesk/views/public.py b/helpdesk/views/public.py index 41d7da2d..0f142c05 100644 --- a/helpdesk/views/public.py +++ b/helpdesk/views/public.py @@ -7,9 +7,12 @@ views/public.py - All public facing views, eg non-staff (no authentication required) views. """ - from django.conf import settings -from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied +from django.core.exceptions import ( + ImproperlyConfigured, + ObjectDoesNotExist, + PermissionDenied, +) from django.http import HttpResponseRedirect from django.shortcuts import render from django.urls import reverse @@ -41,11 +44,11 @@ def create_ticket(request, *args, **kwargs): 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: @@ -56,76 +59,85 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView): def dispatch(self, *args, **kwargs): request = self.request - if not request.user.is_authenticated and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT: - return HttpResponseRedirect(reverse('login')) + if ( + not request.user.is_authenticated + and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT + ): + return HttpResponseRedirect(reverse("login")) - if is_helpdesk_staff(request.user) or \ - (request.user.is_authenticated and - helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE): + if is_helpdesk_staff(request.user) or ( + request.user.is_authenticated + and helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE + ): try: if request.user.usersettings_helpdesk.login_view_ticketlist: - return HttpResponseRedirect(reverse('helpdesk:list')) + return HttpResponseRedirect(reverse("helpdesk:list")) else: - return HttpResponseRedirect(reverse('helpdesk:dashboard')) + return HttpResponseRedirect(reverse("helpdesk:dashboard")) except UserSettings.DoesNotExist: - return HttpResponseRedirect(reverse('helpdesk:dashboard')) + return HttpResponseRedirect(reverse("helpdesk:dashboard")) return super().dispatch(*args, **kwargs) def get_initial(self): initial_data = super().get_initial() # add pre-defined data for public ticket - if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_QUEUE'): + if hasattr(settings, "HELPDESK_PUBLIC_TICKET_QUEUE"): # get the requested queue; return an error if queue not found try: - initial_data['queue'] = Queue.objects.get( + initial_data["queue"] = Queue.objects.get( slug=settings.HELPDESK_PUBLIC_TICKET_QUEUE, - allow_public_submission=True + allow_public_submission=True, ).id except Queue.DoesNotExist as e: logger.fatal( "Public queue '%s' is configured as default but can't be found", - settings.HELPDESK_PUBLIC_TICKET_QUEUE + settings.HELPDESK_PUBLIC_TICKET_QUEUE, ) - 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'): - initial_data['due_date'] = settings.HELPDESK_PUBLIC_TICKET_DUE_DATE + 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"): + initial_data["due_date"] = settings.HELPDESK_PUBLIC_TICKET_DUE_DATE return initial_data 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(',') + 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( + "," + ) return kwargs def form_valid(self, form): request = self.request - if text_is_spam(form.cleaned_data['body'], request): + if text_is_spam(form.cleaned_data["body"], request): # This submission is spam. Let's not save it. - return render(request, template_name='helpdesk/public_spam.html') + 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) + 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'), - ticket.ticket_for_url, - quote(ticket.submitter_email), - ticket.secret_key) + return HttpResponseRedirect( + "%s?ticket=%s&email=%s&key=%s" + % ( + reverse("helpdesk:public_view"), + ticket.ticket_for_url, + quote(ticket.submitter_email), + ticket.secret_key, + ) ) except ValueError: # if someone enters a non-int string for the ticket - return HttpResponseRedirect(reverse('helpdesk:home')) + return HttpResponseRedirect(reverse("helpdesk:home")) class CreateTicketIframeView(BaseCreateTicketView): - template_name = 'helpdesk/public_create_ticket_iframe.html' + template_name = "helpdesk/public_create_ticket_iframe.html" @csrf_exempt @xframe_options_exempt @@ -134,11 +146,11 @@ class CreateTicketIframeView(BaseCreateTicketView): def form_valid(self, form): if super().form_valid(form).status_code == 302: - return HttpResponseRedirect(reverse('helpdesk:success_iframe')) + return HttpResponseRedirect(reverse("helpdesk:success_iframe")) class SuccessIframeView(TemplateView): - template_name = 'helpdesk/success_iframe.html' + template_name = "helpdesk/success_iframe.html" @xframe_options_exempt def dispatch(self, *args, **kwargs): @@ -146,123 +158,140 @@ class SuccessIframeView(TemplateView): class CreateTicketView(BaseCreateTicketView): - template_name = 'helpdesk/public_create_ticket.html' + template_name = "helpdesk/public_create_ticket.html" 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 - form.error_css_class = 'text-danger' + form.error_css_class = "text-danger" return form class Homepage(CreateTicketView): - template_name = 'helpdesk/public_homepage.html' + template_name = "helpdesk/public_homepage.html" 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 class SearchForTicketView(TemplateView): - template_name = 'helpdesk/public_view_form.html' + template_name = "helpdesk/public_view_form.html" def get(self, request, *args, **kwargs): - if hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC: + if ( + hasattr(settings, "HELPDESK_VIEW_A_TICKET_PUBLIC") + and settings.HELPDESK_VIEW_A_TICKET_PUBLIC + ): context = self.get_context_data(**kwargs) return self.render_to_response(context) 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." + ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) request = self.request - email = request.GET.get('email', None) - error_message = kwargs.get('error_message', None) + email = request.GET.get("email", None) + error_message = kwargs.get("error_message", None) - context.update({ - 'ticket': False, - 'email': email, - 'error_message': error_message, - 'helpdesk_settings': helpdesk_settings, - }) + context.update( + { + "ticket": False, + "email": email, + "error_message": error_message, + "helpdesk_settings": helpdesk_settings, + } + ) return context class ViewTicket(TemplateView): - template_name = 'helpdesk/public_view_ticket.html' - + template_name = "helpdesk/public_view_ticket.html" def get(self, request, *args, **kwargs): - ticket_req = request.GET.get('ticket', None) - email = request.GET.get('email', None) - key = request.GET.get('key', '') + ticket_req = request.GET.get("ticket", None) + email = request.GET.get("email", None) + key = request.GET.get("key", "") if not (ticket_req and email): if ticket_req is None and email is None: return SearchForTicketView.as_view()(request) else: - return SearchForTicketView.as_view()(request, _('Missing ticket ID or e-mail address. Please try again.')) + return SearchForTicketView.as_view()( + request, _("Missing ticket ID or e-mail address. Please try again.") + ) try: queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req) if request.user.is_authenticated and request.user.email == email: ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email) - elif hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC: + elif ( + 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) 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 SearchForTicketView.as_view()(request, _('Invalid ticket ID or e-mail address. Please try again.')) + return SearchForTicketView.as_view()( + request, _("Invalid ticket ID or e-mail address. Please try again.") + ) - if 'close' in request.GET and ticket.status == Ticket.RESOLVED_STATUS: + if "close" in request.GET and ticket.status == Ticket.RESOLVED_STATUS: from helpdesk.update_ticket import update_ticket + update_ticket( request.user, ticket, public=True, - comment=_('Submitter accepted resolution and closed ticket'), + comment=_("Submitter accepted resolution and closed ticket"), new_status=Ticket.CLOSED_STATUS, ) return HttpResponseRedirect(ticket.ticket_url) # Prepare context for rendering context = { - 'key': key, - 'mail': email, - 'ticket': ticket, - 'helpdesk_settings': helpdesk_settings, - 'next': self.get_next_url(ticket_id) + "key": key, + "mail": email, + "ticket": ticket, + "helpdesk_settings": helpdesk_settings, + "next": self.get_next_url(ticket_id), } return self.render_to_response(context) def get_next_url(self, ticket_id): - redirect_url = '' + redirect_url = "" if is_helpdesk_staff(self.request.user): - redirect_url = reverse('helpdesk:view', args=[ticket_id]) - if 'close' in self.request.GET: - redirect_url += '?close' + redirect_url = reverse("helpdesk:view", args=[ticket_id]) + if "close" in self.request.GET: + redirect_url += "?close" elif helpdesk_settings.HELPDESK_NAVIGATION_ENABLED: - redirect_url = reverse('helpdesk:view', args=[ticket_id]) + redirect_url = reverse("helpdesk:view", args=[ticket_id]) return redirect_url class MyTickets(TemplateView): - template_name = 'helpdesk/my_tickets.html' + template_name = "helpdesk/my_tickets.html" def get(self, request, *args, **kwargs): if not request.user.is_authenticated: - return HttpResponseRedirect(reverse('helpdesk:login')) + return HttpResponseRedirect(reverse("helpdesk:login")) context = self.get_context_data(**kwargs) return self.render_to_response(context) def change_language(request): - return_to = '' - if 'return_to' in request.GET: - return_to = request.GET['return_to'] + return_to = "" + if "return_to" in request.GET: + return_to = request.GET["return_to"] - return render(request, 'helpdesk/public_change_language.html', {'next': return_to}) + return render(request, "helpdesk/public_change_language.html", {"next": return_to}) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 21544247..ef4e1642 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -37,7 +37,7 @@ from helpdesk.decorators import ( helpdesk_staff_member_required, helpdesk_superuser_required, is_helpdesk_staff, - superuser_required + superuser_required, ) from helpdesk.forms import ( ChecklistForm, @@ -56,7 +56,7 @@ from helpdesk.forms import ( TicketDependencyForm, TicketForm, TicketResolvesForm, - UserSettingsForm + UserSettingsForm, ) from helpdesk.lib import queue_template_context, safe_template_context from helpdesk.models import ( @@ -74,11 +74,15 @@ from helpdesk.models import ( TicketCC, TicketCustomFieldValue, TicketDependency, - UserSettings + UserSettings, ) from helpdesk.query import get_query_class, query_from_base64, query_to_base64 from helpdesk.user import HelpdeskUser -from helpdesk.update_ticket import update_ticket, subscribe_to_ticket_updates, return_ticketccstring_and_show_subscribe +from helpdesk.update_ticket import ( + update_ticket, + subscribe_to_ticket_updates, + return_ticketccstring_and_show_subscribe, +) import helpdesk.views.abstract_views as abstract_views from helpdesk.views.permissions import MustBeStaffMixin import json @@ -92,7 +96,7 @@ if helpdesk_settings.HELPDESK_KB_ENABLED: from helpdesk.models import KBItem DATE_RE: re.Pattern = re.compile( - r'(?P\d{1,2})/(?P\d{1,2})/(?P\d{4})$' + r"(?P\d{1,2})/(?P\d{1,2})/(?P\d{4})$" ) User = get_user_model() @@ -101,10 +105,12 @@ Query = get_query_class() if helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE: # treat 'normal' users like 'staff' staff_member_required = user_passes_test( - lambda u: u.is_authenticated and u.is_active) + lambda u: u.is_authenticated and u.is_active + ) else: staff_member_required = user_passes_test( - lambda u: u.is_authenticated and u.is_active and u.is_staff) + lambda u: u.is_authenticated and u.is_active and u.is_staff + ) def _get_queue_choices(queues): @@ -116,7 +122,7 @@ def _get_queue_choices(queues): queue_choices = [] if len(queues) > 1: - queue_choices = [('', '--------')] + queue_choices = [("", "--------")] queue_choices += [(q.id, q.title) for q in queues] return queue_choices @@ -129,20 +135,23 @@ def dashboard(request): with options for them to 'Take' ownership of said tickets. """ # user settings num tickets per page - if request.user.is_authenticated and hasattr(request.user, 'usersettings_helpdesk'): + if request.user.is_authenticated and hasattr(request.user, "usersettings_helpdesk"): tickets_per_page = request.user.usersettings_helpdesk.tickets_per_page else: tickets_per_page = 25 # 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) + 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) huser = HelpdeskUser(request.user) - active_tickets = Ticket.objects.select_related('queue').exclude( - status__in=[Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS, Ticket.DUPLICATE_STATUS], + active_tickets = Ticket.objects.select_related("queue").exclude( + status__in=[ + Ticket.CLOSED_STATUS, + Ticket.RESOLVED_STATUS, + Ticket.DUPLICATE_STATUS, + ], ) # open & reopened tickets, assigned to current user @@ -151,16 +160,19 @@ def dashboard(request): ) # closed & resolved tickets, assigned to current user - tickets_closed_resolved = Ticket.objects.select_related('queue').filter( + tickets_closed_resolved = Ticket.objects.select_related("queue").filter( assigned_to=request.user, - status__in=[Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS, Ticket.DUPLICATE_STATUS] + status__in=[ + Ticket.CLOSED_STATUS, + Ticket.RESOLVED_STATUS, + Ticket.DUPLICATE_STATUS, + ], ) user_queues = huser.get_queues() unassigned_tickets = active_tickets.filter( - assigned_to__isnull=True, - queue__in=user_queues + assigned_to__isnull=True, queue__in=user_queues ) kbitems = None # Teams mode uses assignment via knowledge base items so exclude tickets assigned to KB items @@ -169,12 +181,16 @@ def dashboard(request): kbitems = huser.get_assigned_kb_items() # all tickets, reported by current user - all_tickets_reported_by_current_user = '' + all_tickets_reported_by_current_user = "" email_current_user = request.user.email if email_current_user: - all_tickets_reported_by_current_user = Ticket.objects.select_related('queue').filter( - submitter_email=email_current_user, - ).order_by('status') + all_tickets_reported_by_current_user = ( + Ticket.objects.select_related("queue") + .filter( + submitter_email=email_current_user, + ) + .order_by("status") + ) tickets_in_queues = Ticket.objects.filter( queue__in=user_queues, @@ -197,48 +213,46 @@ def dashboard(request): # where_clause = """WHERE q.id = t.queue_id""" # get user assigned tickets page - paginator = Paginator( - tickets, tickets_per_page) + paginator = Paginator(tickets, tickets_per_page) try: tickets = paginator.page(user_tickets_page) except PageNotAnInteger: tickets = paginator.page(1) except EmptyPage: - tickets = paginator.page( - paginator.num_pages) + tickets = paginator.page(paginator.num_pages) # get user completed tickets page - paginator = Paginator( - tickets_closed_resolved, tickets_per_page) + paginator = Paginator(tickets_closed_resolved, tickets_per_page) try: - tickets_closed_resolved = paginator.page( - user_tickets_closed_resolved_page) + tickets_closed_resolved = paginator.page(user_tickets_closed_resolved_page) except PageNotAnInteger: tickets_closed_resolved = paginator.page(1) except EmptyPage: - tickets_closed_resolved = paginator.page( - paginator.num_pages) + tickets_closed_resolved = paginator.page(paginator.num_pages) # get user submitted tickets page - paginator = Paginator( - all_tickets_reported_by_current_user, tickets_per_page) + paginator = Paginator(all_tickets_reported_by_current_user, tickets_per_page) try: all_tickets_reported_by_current_user = paginator.page( - all_tickets_reported_by_current_user_page) + all_tickets_reported_by_current_user_page + ) except PageNotAnInteger: all_tickets_reported_by_current_user = paginator.page(1) except EmptyPage: - all_tickets_reported_by_current_user = paginator.page( - paginator.num_pages) + all_tickets_reported_by_current_user = paginator.page(paginator.num_pages) - return render(request, 'helpdesk/dashboard.html', { - 'user_tickets': tickets, - 'user_tickets_closed_resolved': tickets_closed_resolved, - 'unassigned_tickets': unassigned_tickets, - 'kbitems': kbitems, - 'all_tickets_reported_by_current_user': all_tickets_reported_by_current_user, - 'basic_ticket_stats': basic_ticket_stats, - }) + return render( + request, + "helpdesk/dashboard.html", + { + "user_tickets": tickets, + "user_tickets_closed_resolved": tickets_closed_resolved, + "unassigned_tickets": unassigned_tickets, + "kbitems": kbitems, + "all_tickets_reported_by_current_user": all_tickets_reported_by_current_user, + "basic_ticket_stats": basic_ticket_stats, + }, + ) dashboard = staff_member_required(dashboard) @@ -257,16 +271,17 @@ def delete_ticket(request, ticket_id): ticket = get_object_or_404(Ticket, id=ticket_id) ticket_perm_check(request, ticket) - if request.method == 'GET': - return render(request, 'helpdesk/delete_ticket.html', { - 'ticket': ticket, - 'next': request.GET.get('next', 'home') - }) + if request.method == "GET": + return render( + request, + "helpdesk/delete_ticket.html", + {"ticket": ticket, "next": request.GET.get("next", "home")}, + ) else: ticket.delete() - redirect_to = 'helpdesk:home' - if request.POST.get('next') == 'dashboard': - redirect_to = 'helpdesk:dashboard' + redirect_to = "helpdesk:home" + if request.POST.get("next") == "dashboard": + redirect_to = "helpdesk:dashboard" return HttpResponseRedirect(reverse(redirect_to)) @@ -280,39 +295,52 @@ def followup_edit(request, ticket_id, followup_id): ticket = get_object_or_404(Ticket, id=ticket_id) ticket_perm_check(request, ticket) - if request.method == 'GET': - form = EditFollowUpForm(initial={ - 'title': escape(followup.title), - 'ticket': followup.ticket, - 'comment': escape(followup.comment), - 'public': followup.public, - 'new_status': followup.new_status, - 'time_spent': format_time_spent(followup.time_spent), - }) + if request.method == "GET": + form = EditFollowUpForm( + initial={ + "title": escape(followup.title), + "ticket": followup.ticket, + "comment": escape(followup.comment), + "public": followup.public, + "new_status": followup.new_status, + "time_spent": format_time_spent(followup.time_spent), + } + ) - ticketcc_string = return_ticketccstring_and_show_subscribe(request.user, ticket)[0] + ticketcc_string = return_ticketccstring_and_show_subscribe( + request.user, ticket + )[0] - return render(request, 'helpdesk/followup_edit.html', { - 'followup': followup, - 'ticket': ticket, - 'form': form, - 'ticketcc_string': ticketcc_string, - }) - elif request.method == 'POST': + return render( + request, + "helpdesk/followup_edit.html", + { + "followup": followup, + "ticket": ticket, + "form": form, + "ticketcc_string": ticketcc_string, + }, + ) + elif request.method == "POST": form = EditFollowUpForm(request.POST) if form.is_valid(): - title = form.cleaned_data['title'] - _ticket = form.cleaned_data['ticket'] - comment = form.cleaned_data['comment'] - public = form.cleaned_data['public'] - new_status = form.cleaned_data['new_status'] - time_spent = form.cleaned_data['time_spent'] + title = form.cleaned_data["title"] + _ticket = form.cleaned_data["ticket"] + comment = form.cleaned_data["comment"] + public = form.cleaned_data["public"] + new_status = form.cleaned_data["new_status"] + time_spent = form.cleaned_data["time_spent"] # will save previous date old_date = followup.date - new_followup = FollowUp(title=title, date=old_date, ticket=_ticket, - comment=comment, public=public, - new_status=new_status, - time_spent=time_spent) + new_followup = FollowUp( + title=title, + date=old_date, + ticket=_ticket, + comment=comment, + public=public, + new_status=new_status, + time_spent=time_spent, + ) # keep old user if one did exist before. if followup.user: new_followup.user = followup.user @@ -324,7 +352,7 @@ def followup_edit(request, ticket_id, followup_id): attachment.save() # delete old followup followup.delete() - return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket.id])) + return HttpResponseRedirect(reverse("helpdesk:view", args=[ticket.id])) followup_edit = staff_member_required(followup_edit) @@ -336,11 +364,11 @@ def followup_delete(request, ticket_id, followup_id): ticket = get_object_or_404(Ticket, id=ticket_id) if not request.user.is_superuser: - return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket.id])) + return HttpResponseRedirect(reverse("helpdesk:view", args=[ticket.id])) followup = get_object_or_404(FollowUp, id=followup_id) followup.delete() - return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket.id])) + return HttpResponseRedirect(reverse("helpdesk:view", args=[ticket.id])) followup_delete = staff_member_required(followup_delete) @@ -351,26 +379,22 @@ def view_ticket(request, ticket_id): ticket = get_object_or_404(Ticket, id=ticket_id) ticket_perm_check(request, ticket) - if 'take' in request.GET: - update_ticket( - request.user, - ticket, - owner=request.user.id - ) + if "take" in request.GET: + update_ticket(request.user, ticket, owner=request.user.id) return return_to_ticket(request.user, helpdesk_settings, ticket) - if 'subscribe' in request.GET: + if "subscribe" in request.GET: # Allow the user to subscribe him/herself to the ticket whilst viewing # it. - show_subscribe = return_ticketccstring_and_show_subscribe( - request.user, ticket - )[1] + show_subscribe = return_ticketccstring_and_show_subscribe(request.user, ticket)[ + 1 + ] if show_subscribe: subscribe_to_ticket_updates(ticket, request.user) - return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket.id])) + return HttpResponseRedirect(reverse("helpdesk:view", args=[ticket.id])) - if 'close' in request.GET and ticket.status == Ticket.RESOLVED_STATUS: + if "close" in request.GET and ticket.status == Ticket.RESOLVED_STATUS: if not ticket.assigned_to: owner = 0 else: @@ -380,33 +404,36 @@ def view_ticket(request, ticket_id): request.user, ticket, owner=owner, - comment= _('Accepted resolution and closed ticket'), + comment=_("Accepted resolution and closed ticket"), ) return return_to_ticket(request.user, helpdesk_settings, ticket) 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) # TODO: shouldn't this template get a form to begin with? - form = TicketForm(initial={'due_date': ticket.due_date}, - queue_choices=queue_choices) + form = TicketForm( + initial={"due_date": ticket.due_date}, queue_choices=queue_choices + ) - ticketcc_string, show_subscribe = \ - return_ticketccstring_and_show_subscribe(request.user, ticket) + ticketcc_string, show_subscribe = return_ticketccstring_and_show_subscribe( + request.user, ticket + ) submitter_userprofile = ticket.get_submitter_userprofile() 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), - kwargs={'object_id': submitter_userprofile.id} + "admin:{app}_{model}_change".format( + app=content_type.app_label, model=content_type.model + ), + kwargs={"object_id": submitter_userprofile.id}, ) else: submitter_userprofile_url = None @@ -417,38 +444,41 @@ def view_ticket(request, ticket_id): checklist.ticket = ticket checklist.save() - checklist_template = checklist_form.cleaned_data.get('checklist_template') + checklist_template = checklist_form.cleaned_data.get("checklist_template") # Add predefined tasks if template has been selected if checklist_template: checklist.create_tasks_from_template(checklist_template) - return redirect('helpdesk:edit_ticket_checklist', ticket.id, checklist.id) + return redirect("helpdesk:edit_ticket_checklist", ticket.id, checklist.id) # List open tickets on top dependencies = ticket.ticketdependency.annotate( - rank=Case( - When(depends_on__status__in=Ticket.OPEN_STATUSES, then=1), - default=2 - )).order_by('rank') - + rank=Case(When(depends_on__status__in=Ticket.OPEN_STATUSES, then=1), default=2) + ).order_by("rank") + # add custom fields to further details panel customfields_form = EditTicketCustomFieldForm(None, instance=ticket) - return render(request, 'helpdesk/ticket.html', { - 'ticket': ticket, - 'dependencies': dependencies, - 'submitter_userprofile_url': submitter_userprofile_url, - 'form': form, - 'active_users': users, - 'priorities': Ticket.PRIORITY_CHOICES, - 'queues': queue_choices, - 'preset_replies': PreSetReply.objects.filter( - Q(queues=ticket.queue) | Q(queues__isnull=True)), - 'ticketcc_string': ticketcc_string, - 'SHOW_SUBSCRIBE': show_subscribe, - 'checklist_form': checklist_form, - 'customfields_form': customfields_form, - }) + return render( + request, + "helpdesk/ticket.html", + { + "ticket": ticket, + "dependencies": dependencies, + "submitter_userprofile_url": submitter_userprofile_url, + "form": form, + "active_users": users, + "priorities": Ticket.PRIORITY_CHOICES, + "queues": queue_choices, + "preset_replies": PreSetReply.objects.filter( + Q(queues=ticket.queue) | Q(queues__isnull=True) + ), + "ticketcc_string": ticketcc_string, + "SHOW_SUBSCRIBE": show_subscribe, + "checklist_form": checklist_form, + "customfields_form": customfields_form, + }, + ) @helpdesk_staff_member_required @@ -462,13 +492,13 @@ def edit_ticket_checklist(request, ticket_id, checklist_id): Checklist, ChecklistTask, formset=FormControlDeleteFormSet, - fields=['description', 'position'], + fields=["description", "position"], widgets={ - 'position': HiddenInput(), - 'description': TextInput(attrs={'class': 'form-control'}), + "position": HiddenInput(), + "description": TextInput(attrs={"class": "form-control"}), }, can_delete=True, - extra=0 + extra=0, ) formset = TaskFormSet(request.POST or None, instance=checklist) if form.is_valid() and formset.is_valid(): @@ -476,12 +506,16 @@ def edit_ticket_checklist(request, ticket_id, checklist_id): formset.save() return redirect(ticket) - return render(request, 'helpdesk/checklist_form.html', { - 'ticket': ticket, - 'checklist': checklist, - 'form': form, - 'formset': formset, - }) + return render( + request, + "helpdesk/checklist_form.html", + { + "ticket": ticket, + "checklist": checklist, + "form": form, + "formset": formset, + }, + ) @helpdesk_staff_member_required @@ -490,20 +524,22 @@ def delete_ticket_checklist(request, ticket_id, checklist_id): ticket_perm_check(request, ticket) checklist = get_object_or_404(ticket.checklists.all(), id=checklist_id) - if request.method == 'POST': + if request.method == "POST": checklist.delete() return redirect(ticket) - return render(request, 'helpdesk/checklist_confirm_delete.html', { - 'ticket': ticket, - 'checklist': checklist, - }) + return render( + request, + "helpdesk/checklist_confirm_delete.html", + { + "ticket": ticket, + "checklist": checklist, + }, + ) def get_ticket_from_request_with_authorisation( - request: WSGIRequest, - ticket_id: str, - public: bool + request: WSGIRequest, ticket_id: str, public: bool ) -> Ticket: """Gets a ticket from the public status and if the user is authenticated and has permissions to update tickets @@ -512,17 +548,22 @@ def get_ticket_from_request_with_authorisation( Http404 when the ticket can not be found or the user lacks permission """ - if not (public or ( - request.user.is_authenticated and - request.user.is_active and ( - is_helpdesk_staff(request.user) or - helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE))): - + if not ( + public + or ( + request.user.is_authenticated + and request.user.is_active + and ( + is_helpdesk_staff(request.user) + or helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE + ) + ) + ): try: return Ticket.objects.get( id=ticket_id, - submitter_email__iexact=request.POST.get('mail'), - secret_key__iexact=request.POST.get('key') + submitter_email__iexact=request.POST.get("mail"), + secret_key__iexact=request.POST.get("key"), ) except (Ticket.DoesNotExist, ValueError): raise PermissionDenied() @@ -531,21 +572,22 @@ def get_ticket_from_request_with_authorisation( def get_due_date_from_request_or_ticket( - request: WSGIRequest, - ticket: Ticket + request: WSGIRequest, ticket: Ticket ) -> typing.Optional[datetime.date]: """Tries to locate the due date for a ticket from the `request.POST` 'due_date' parameter or the `due_date_*` paramaters. """ - due_date = request.POST.get('due_date', None) or None + due_date = request.POST.get("due_date", None) or None if due_date is not None: - parsed_date = parse_datetime(due_date) or datetime.combine(parse_date(due_date), time()) + parsed_date = parse_datetime(due_date) or datetime.combine( + parse_date(due_date), time() + ) due_date = make_aware(parsed_date) else: - due_date_year = int(request.POST.get('due_date_year', 0)) - due_date_month = int(request.POST.get('due_date_month', 0)) - due_date_day = int(request.POST.get('due_date_day', 0)) + due_date_year = int(request.POST.get("due_date_year", 0)) + due_date_month = int(request.POST.get("due_date_month", 0)) + due_date_day = int(request.POST.get("due_date_day", 0)) # old way, probably deprecated? if not (due_date_year and due_date_month and due_date_day): due_date = ticket.due_date @@ -556,43 +598,41 @@ def get_due_date_from_request_or_ticket( 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) return due_date def get_time_spent_from_request(request: WSGIRequest) -> typing.Optional[timedelta]: 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(":")] return timedelta(hours=hours, minutes=minutes) return None def update_ticket_view(request, ticket_id, public=False): - try: ticket = get_ticket_from_request_with_authorisation(request, ticket_id, public) except PermissionDenied: - return redirect_to_login(request.path, 'helpdesk:login') + return redirect_to_login(request.path, "helpdesk:login") - comment = request.POST.get('comment', '') - new_status = int(request.POST.get('new_status', ticket.status)) - title = request.POST.get('title', ticket.title) - owner = int(request.POST.get('owner', -1)) - priority = int(request.POST.get('priority', ticket.priority)) - queue = int(request.POST.get('queue', ticket.queue.id)) + comment = request.POST.get("comment", "") + new_status = int(request.POST.get("new_status", ticket.status)) + title = request.POST.get("title", ticket.title) + owner = int(request.POST.get("owner", -1)) + priority = int(request.POST.get("priority", ticket.priority)) + queue = int(request.POST.get("queue", ticket.queue.id)) # custom fields - customfields_form = EditTicketCustomFieldForm(request.POST or None, - instance=ticket) + customfields_form = EditTicketCustomFieldForm(request.POST or None, instance=ticket) # Check if a change happened on checklists new_checklists = {} changes_in_checklists = False for checklist in ticket.checklists.all(): - old_completed = set(checklist.tasks.completed().values_list('id', flat=True)) - new_checklist = set(map(int, request.POST.getlist(f'checklist-{checklist.id}', []))) + old_completed = set(checklist.tasks.completed().values_list("id", flat=True)) + new_checklist = set( + map(int, request.POST.getlist(f"checklist-{checklist.id}", [])) + ) new_checklists[checklist.id] = new_checklist if new_checklist != old_completed: changes_in_checklists = True @@ -602,37 +642,40 @@ def update_ticket_view(request, ticket_id, public=False): # very US-centric but for now that's the only format supported # until we clean up code to internationalize a little more due_date = get_due_date_from_request_or_ticket(request, ticket) - no_changes = all([ - not request.FILES, - not comment, - not changes_in_checklists, - new_status == ticket.status, - title == ticket.title, - priority == int(ticket.priority), - queue == int(ticket.queue.id), - due_date == ticket.due_date, - (owner == -1) or (not owner and not ticket.assigned_to) or - (owner and User.objects.get(id=owner) == ticket.assigned_to), - not customfields_form.has_changed(), - ]) + no_changes = all( + [ + not request.FILES, + not comment, + not changes_in_checklists, + new_status == ticket.status, + title == ticket.title, + priority == int(ticket.priority), + queue == int(ticket.queue.id), + due_date == ticket.due_date, + (owner == -1) + or (not owner and not ticket.assigned_to) + or (owner and User.objects.get(id=owner) == ticket.assigned_to), + not customfields_form.has_changed(), + ] + ) if no_changes: return return_to_ticket(request.user, helpdesk_settings, ticket) update_ticket( request.user, ticket, - title = title, - comment = comment, - files = request.FILES.getlist('attachment'), - public = request.POST.get('public', False), - owner = int(request.POST.get('owner', -1)), - priority = int(request.POST.get('priority', -1)), - queue = int(request.POST.get('queue', -1)), - new_status = new_status, - time_spent = get_time_spent_from_request(request), - due_date = get_due_date_from_request_or_ticket(request, ticket), - new_checklists = new_checklists, - customfields_form = customfields_form, + title=title, + comment=comment, + files=request.FILES.getlist("attachment"), + public=request.POST.get("public", False), + owner=int(request.POST.get("owner", -1)), + priority=int(request.POST.get("priority", -1)), + queue=int(request.POST.get("queue", -1)), + new_status=new_status, + time_spent=get_time_spent_from_request(request), + due_date=get_due_date_from_request_or_ticket(request, ticket), + new_checklists=new_checklists, + customfields_form=customfields_form, ) return return_to_ticket(request.user, helpdesk_settings, ticket) @@ -649,32 +692,33 @@ def return_to_ticket(user, helpdesk_settings, ticket): @helpdesk_staff_member_required def mass_update(request): - tickets = request.POST.getlist('ticket_id') - action = request.POST.get('action', None) + tickets = request.POST.getlist("ticket_id") + action = request.POST.get("action", None) if not (tickets and action): - return HttpResponseRedirect(reverse('helpdesk:list')) + return HttpResponseRedirect(reverse("helpdesk:list")) user = kbitem = None - if action.startswith('assign_'): - parts = action.split('_') + if action.startswith("assign_"): + parts = action.split("_") user = User.objects.get(id=parts[1]) - action = 'assign' - if action == 'kbitem_none': - action = 'set_kbitem' - if action.startswith('kbitem_'): - parts = action.split('_') + action = "assign" + if action == "kbitem_none": + action = "set_kbitem" + if action.startswith("kbitem_"): + parts = action.split("_") kbitem = KBItem.objects.get(id=parts[1]) - action = 'set_kbitem' - elif action == 'take': + action = "set_kbitem" + elif action == "take": user = request.user - action = 'assign' - elif action == 'merge': + action = "assign" + elif action == "merge": # 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) @@ -682,57 +726,61 @@ def mass_update(request): if not huser.can_access_queue(t.queue): continue - if action == 'assign' and t.assigned_to != user: + if action == "assign" and t.assigned_to != user: t.assigned_to = user t.save() t.followup_set.create( date=timezone.now(), - title=_('Assigned to %(username)s in bulk update' % {'username': user.get_username()}), + title=_( + "Assigned to %(username)s in bulk update" + % {"username": user.get_username()} + ), public=True, - user=request.user + user=request.user, ) - elif action == 'unassign' and t.assigned_to is not None: + elif action == "unassign" and t.assigned_to is not None: t.assigned_to = None t.save() t.followup_set.create( date=timezone.now(), - title=_('Unassigned in bulk update'), + title=_("Unassigned in bulk update"), public=True, - user=request.user + user=request.user, ) - elif action == 'set_kbitem': + elif action == "set_kbitem": t.kbitem = kbitem t.save() t.followup_set.create( date=timezone.now(), - title=_('KBItem set in bulk update'), - public=False, - user=request.user - ) - elif action == 'close' and t.status != Ticket.CLOSED_STATUS: - t.status = Ticket.CLOSED_STATUS - t.save() - t.followup_set.create( - date=timezone.now(), - title=_('Closed in bulk update'), + title=_("KBItem set in bulk update"), public=False, user=request.user, - new_status=Ticket.CLOSED_STATUS ) - elif action == 'close_public' and t.status != Ticket.CLOSED_STATUS: + elif action == "close" and t.status != Ticket.CLOSED_STATUS: t.status = Ticket.CLOSED_STATUS t.save() t.followup_set.create( date=timezone.now(), - title=_('Closed in bulk update'), + title=_("Closed in bulk update"), + public=False, + user=request.user, + new_status=Ticket.CLOSED_STATUS, + ) + elif action == "close_public" and t.status != Ticket.CLOSED_STATUS: + t.status = Ticket.CLOSED_STATUS + t.save() + t.followup_set.create( + date=timezone.now(), + title=_("Closed in bulk update"), public=True, user=request.user, - new_status=Ticket.CLOSED_STATUS + new_status=Ticket.CLOSED_STATUS, ) # Send email to Submitter, Owner, Queue CC context = safe_template_context(t) - context.update(resolution=t.resolution, - queue=queue_template_context(t.queue)) + context.update( + resolution=t.resolution, queue=queue_template_context(t.queue) + ) messages_sent_to = set() try: @@ -741,22 +789,27 @@ def mass_update(request): pass roles = { - 'submitter': ('closed_submitter', context), - 'ticket_cc': ('closed_cc', context), + "submitter": ("closed_submitter", context), + "ticket_cc": ("closed_cc", context), } - if t.assigned_to and t.assigned_to.usersettings_helpdesk.email_on_ticket_change: - roles['assigned_to'] = ('closed_owner', context) + if ( + t.assigned_to + and t.assigned_to.usersettings_helpdesk.email_on_ticket_change + ): + roles["assigned_to"] = ("closed_owner", context) - messages_sent_to.update(t.send( - roles, - dont_send_to=messages_sent_to, - fail_silently=True, - )) + messages_sent_to.update( + t.send( + roles, + dont_send_to=messages_sent_to, + fail_silently=True, + ) + ) - elif action == 'delete': + elif action == "delete": t.delete() - return HttpResponseRedirect(reverse('helpdesk:list')) + return HttpResponseRedirect(reverse("helpdesk:list")) mass_update = staff_member_required(mass_update) @@ -765,20 +818,18 @@ 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 TICKET_ATTRIBUTES = ( - ('created', _('Created date')), - ('due_date', _('Due on')), - ('get_status_display', _('Status')), - ('submitter_email', _('Submitter email')), - ('assigned_to', _('Owner')), - ('description', _('Description')), - ('resolution', _('Resolution')), + ("created", _("Created date")), + ("due_date", _("Due on")), + ("get_status_display", _("Status")), + ("submitter_email", _("Submitter email")), + ("assigned_to", _("Owner")), + ("description", _("Description")), + ("resolution", _("Resolution")), ) def merge_ticket_values( - request: WSGIRequest, - tickets: typing.List[Ticket], - custom_fields + request: WSGIRequest, tickets: typing.List[Ticket], custom_fields ) -> None: for ticket in tickets: ticket.values = {} @@ -786,31 +837,29 @@ def merge_ticket_values( for attribute, __ in TICKET_ATTRIBUTES: value = getattr(ticket, attribute, TicketCustomFieldValue.default_value) # Check if attr is a get_FIELD_display - if attribute.startswith('get_') and attribute.endswith('_display'): + if attribute.startswith("get_") and attribute.endswith("_display"): # Hack to call methods like get_FIELD_display() - value = getattr(ticket, attribute, TicketCustomFieldValue.default_value)() + value = getattr( + ticket, attribute, TicketCustomFieldValue.default_value + )() ticket.values[attribute] = { - 'value': value, - 'checked': str(ticket.id) == request.POST.get(attribute) + "value": value, + "checked": str(ticket.id) == request.POST.get(attribute), } # 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 = TicketCustomFieldValue.default_value ticket.values[custom_field.name] = { - 'value': value, - 'checked': str(ticket.id) == request.POST.get(custom_field.name) + "value": value, + "checked": str(ticket.id) == request.POST.get(custom_field.name), } def redirect_from_chosen_ticket( - request, - chosen_ticket, - tickets, - custom_fields + request, chosen_ticket, tickets, custom_fields ) -> HttpResponseRedirect: # Save ticket fields values for attribute, __ in TICKET_ATTRIBUTES: @@ -822,7 +871,7 @@ def redirect_from_chosen_ticket( continue # Check if attr is a get_FIELD_display - if attribute.startswith('get_') and attribute.endswith('_display'): + 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 @@ -834,8 +883,7 @@ def redirect_from_chosen_ticket( 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 @@ -843,19 +891,21 @@ def redirect_from_chosen_ticket( # exists try: value = selected_ticket.ticketcustomfieldvalue_set.get( - field=custom_field).value + field=custom_field + ).value except TicketCustomFieldValue.DoesNotExist: continue # 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} + custom_field_value, created = ( + chosen_ticket.ticketcustomfieldvalue_set.get_or_create( + field=custom_field, defaults={"value": value} + ) ) if not created: custom_field_value.value = value - custom_field_value.save(update_fields=['value']) + custom_field_value.save(update_fields=["value"]) # Save changes chosen_ticket.save() @@ -871,13 +921,15 @@ def redirect_from_chosen_ticket( context = safe_template_context(ticket) if ticket.submitter_email: send_templated_mail( - template_name='merged', + template_name="merged", context=context, recipients=[ticket.submitter_email], bcc=[ - cc.email_address for cc in ticket.ticketcc_set.select_related('user')], + cc.email_address + for cc in ticket.ticketcc_set.select_related("user") + ], sender=ticket.queue.from_address, - fail_silently=True + fail_silently=True, ) # Move all followups and update their title to know they @@ -885,20 +937,19 @@ def redirect_from_chosen_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) + 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) + 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) @@ -913,37 +964,38 @@ def merge_tickets(request): ticket_select_form = MultipleTicketSelectForm(request.GET or None) tickets = custom_fields = None if ticket_select_form.is_valid(): - tickets = ticket_select_form.cleaned_data.get('tickets') + tickets = ticket_select_form.cleaned_data.get("tickets") custom_fields = CustomField.objects.all() merge_ticket_values(request, tickets, custom_fields) - if request.method == 'POST': + 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', + field="tickets", error=_( - 'Please choose a ticket in which the others will be merged into.') + "Please choose a ticket in which the others will be merged into." + ), ) else: return redirect_from_chosen_ticket( - request, - chosen_ticket, - tickets, - custom_fields + request, chosen_ticket, tickets, custom_fields ) - return render(request, 'helpdesk/ticket_merge.html', { - 'tickets': tickets, - 'ticket_attributes': TICKET_ATTRIBUTES, - 'custom_fields': custom_fields, - 'ticket_select_form': ticket_select_form - }) + return render( + request, + "helpdesk/ticket_merge.html", + { + "tickets": tickets, + "ticket_attributes": TICKET_ATTRIBUTES, + "custom_fields": custom_fields, + "ticket_select_form": ticket_select_form, + }, + ) def check_redirect_on_user_query(request, huser): @@ -952,24 +1004,24 @@ def check_redirect_on_user_query(request, huser): they have, just redirect to that ticket number. Otherwise, we treat it as a keyword search. """ - if request.GET.get('search_type', None) == 'header': - query = request.GET.get('q') + if request.GET.get("search_type", None) == "header": + query = request.GET.get("q") filter_ = None - if query.find('-') > 0: + if query.find("-") > 0: try: queue, id_ = Ticket.queue_and_id_from_query(query) id_ = int(id_) except ValueError: pass else: - filter_ = {'queue__slug': queue, 'id': id_} + filter_ = {"queue__slug": queue, "id": id_} else: try: query = int(query) except ValueError: pass else: - filter_ = {'id': int(query)} + filter_ = {"id": int(query)} if filter_: try: @@ -990,19 +1042,19 @@ def ticket_list(request): # Query_params will hold a dictionary of parameters relating to # a query, to be saved if needed: query_params = { - 'filtering': {}, - 'filtering_null': {}, - 'sorting': None, - 'sortreverse': False, - 'search_string': '', + "filtering": {}, + "filtering_null": {}, + "sorting": None, + "sortreverse": False, + "search_string": "", } default_query_params = { - 'filtering': { - 'status__in': [1, 2], + "filtering": { + "status__in": [1, 2], }, - 'sorting': 'created', - 'search_string': '', - 'sortreverse': False, + "sorting": "created", + "search_string": "", + "sortreverse": False, } #: check for a redirect, see function doc for details @@ -1012,26 +1064,36 @@ def ticket_list(request): try: saved_query, query_params = load_saved_query(request, query_params) except QueryLoadError: - return HttpResponseRedirect(reverse('helpdesk:list')) + return HttpResponseRedirect(reverse("helpdesk:list")) if saved_query: pass - elif not {'queue', 'assigned_to', 'status', 'q', 'sort', 'sortreverse', 'kbitem'}.intersection(request.GET): + elif not { + "queue", + "assigned_to", + "status", + "q", + "sort", + "sortreverse", + "kbitem", + }.intersection(request.GET): # Fall-back if no querying is being done query_params = deepcopy(default_query_params) else: filter_in_params = [ - ('queue', 'queue__id__in'), - ('assigned_to', 'assigned_to__id__in'), - ('status', 'status__in'), - ('kbitem', 'kbitem__in'), + ("queue", "queue__id__in"), + ("assigned_to", "assigned_to__id__in"), + ("status", "status__in"), + ("kbitem", "kbitem__in"), ] - filter_null_params = dict([ - ('queue', 'queue__id__isnull'), - ('assigned_to', 'assigned_to__id__isnull'), - ('status', 'status__isnull'), - ('kbitem', 'kbitem__isnull'), - ]) + filter_null_params = dict( + [ + ("queue", "queue__id__isnull"), + ("assigned_to", "assigned_to__id__isnull"), + ("status", "status__isnull"), + ("kbitem", "kbitem__isnull"), + ] + ) for param, filter_command in filter_in_params: if request.GET.get(param) is not None: patterns = request.GET.getlist(param) @@ -1041,7 +1103,7 @@ def ticket_list(request): minus_1_ndx = patterns.index("-1") # Must have the value so remove it and configure to use OR filter on NULL patterns.pop(minus_1_ndx) - query_params['filtering_null'][filter_null_params[param]] = True + query_params["filtering_null"][filter_null_params[param]] = True except ValueError: pass if not patterns: @@ -1049,71 +1111,87 @@ def ticket_list(request): continue try: pattern_pks = [int(pattern) for pattern in patterns] - query_params['filtering'][filter_command] = pattern_pks + query_params["filtering"][filter_command] = pattern_pks except ValueError: pass - date_from = request.GET.get('date_from') + date_from = request.GET.get("date_from") if date_from: - query_params['filtering']['created__gte'] = date_from + query_params["filtering"]["created__gte"] = date_from - date_to = request.GET.get('date_to') + date_to = request.GET.get("date_to") if date_to: - query_params['filtering']['created__lte'] = date_to + query_params["filtering"]["created__lte"] = date_to # KEYWORD SEARCHING - q = request.GET.get('q', '') - context['query'] = q - query_params['search_string'] = q + q = request.GET.get("q", "") + context["query"] = q + query_params["search_string"] = q # SORTING - sort = request.GET.get('sort', None) - if sort not in ('status', 'assigned_to', 'created', 'title', 'queue', 'priority', 'last_followup', 'kbitem'): - sort = 'created' - query_params['sorting'] = sort + sort = request.GET.get("sort", None) + if sort not in ( + "status", + "assigned_to", + "created", + "title", + "queue", + "priority", + "last_followup", + "kbitem", + ): + sort = "created" + query_params["sorting"] = sort - sortreverse = request.GET.get('sortreverse', None) - query_params['sortreverse'] = sortreverse + sortreverse = request.GET.get("sortreverse", None) + query_params["sortreverse"] = sortreverse urlsafe_query = query_to_base64(query_params) user_saved_queries = SavedSearch.objects.filter( - Q(user=request.user) | Q(shared__exact=True)) + Q(user=request.user) | Q(shared__exact=True) + ) - search_message = '' - if query_params['search_string'] and settings.DATABASES['default']['ENGINE'].endswith('sqlite'): + search_message = "" + if query_params["search_string"] and settings.DATABASES["default"][ + "ENGINE" + ].endswith("sqlite"): search_message = _( - '

    Note: Your keyword search is case sensitive ' - 'because of your database. This means the search will not ' - 'be accurate. By switching to a different database system you will gain ' - 'better searching! For more information, read the ' + "

    Note: Your keyword search is case sensitive " + "because of your database. This means the search will not " + "be accurate. By switching to a different database system you will gain " + "better searching! For more information, read the " '' - 'Django Documentation on string matching in SQLite.') + "Django Documentation on string matching in SQLite." + ) kbitem_choices = [] 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( - context, - default_tickets_per_page=request.user.usersettings_helpdesk.tickets_per_page, - user_choices=User.objects.filter(is_active=True, is_staff=True), - kb_items=kbitem, - queue_choices=huser.get_queues(), - status_choices=Ticket.STATUS_CHOICES, - kbitem_choices=kbitem_choices, - urlsafe_query=urlsafe_query, - user_saved_queries=user_saved_queries, - query_params=query_params, - from_saved_query=saved_query is not None, - saved_query=saved_query, - search_message=search_message, - helpdesk_settings=helpdesk_settings, - )) + return render( + request, + "helpdesk/ticket_list.html", + dict( + context, + default_tickets_per_page=request.user.usersettings_helpdesk.tickets_per_page, + user_choices=User.objects.filter(is_active=True, is_staff=True), + kb_items=kbitem, + queue_choices=huser.get_queues(), + status_choices=Ticket.STATUS_CHOICES, + kbitem_choices=kbitem_choices, + urlsafe_query=urlsafe_query, + user_saved_queries=user_saved_queries, + query_params=query_params, + from_saved_query=saved_query is not None, + saved_query=saved_query, + search_message=search_message, + helpdesk_settings=helpdesk_settings, + ), + ) ticket_list = staff_member_required(ticket_list) @@ -1126,11 +1204,11 @@ class QueryLoadError(Exception): def load_saved_query(request, query_params=None): saved_query = None - if request.GET.get('saved_query', 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() @@ -1138,7 +1216,7 @@ def load_saved_query(request, query_params=None): try: # we get a string like: b'stuff' # so leave of the first two chars (b') and last (') - if saved_query.query.startswith('b\''): + if saved_query.query.startswith("b'"): b64query = saved_query.query[2:-1] else: b64query = saved_query.query @@ -1149,7 +1227,7 @@ def load_saved_query(request, query_params=None): @helpdesk_staff_member_required -@api_view(['GET']) +@api_view(["GET"]) def datatables_ticket_list(request, query): """ Datatable on ticket_list.html uses this view from to get objects to display @@ -1162,7 +1240,7 @@ def datatables_ticket_list(request, query): @helpdesk_staff_member_required -@api_view(['GET']) +@api_view(["GET"]) def timeline_ticket_list(request, query): query = Query(HelpdeskUser(request.user), base64query=query) return JsonResponse(query.get_timeline_context(), status=status.HTTP_200_OK) @@ -1178,14 +1256,20 @@ def edit_ticket(request, ticket_id): ticket = form.save() return redirect(ticket) - return render(request, 'helpdesk/edit_ticket.html', {'form': form, 'ticket': ticket, 'errors': form.errors}) + return render( + request, + "helpdesk/edit_ticket.html", + {"form": form, "ticket": ticket, "errors": form.errors}, + ) edit_ticket = staff_member_required(edit_ticket) -class CreateTicketView(MustBeStaffMixin, abstract_views.AbstractCreateTicketMixin, FormView): - template_name = 'helpdesk/create_ticket.html' +class CreateTicketView( + MustBeStaffMixin, abstract_views.AbstractCreateTicketMixin, FormView +): + template_name = "helpdesk/create_ticket.html" form_class = TicketForm def get_initial(self): @@ -1200,7 +1284,8 @@ class CreateTicketView(MustBeStaffMixin, abstract_views.AbstractCreateTicketMixi def form_valid(self, form): self.ticket = form.save( - user=self.request.user if self.request.user.is_authenticated else None) + user=self.request.user if self.request.user.is_authenticated else None + ) return super().form_valid(form) def get_success_url(self): @@ -1208,7 +1293,7 @@ class CreateTicketView(MustBeStaffMixin, abstract_views.AbstractCreateTicketMixi if HelpdeskUser(request.user).can_access_queue(self.ticket.queue): return self.ticket.get_absolute_url() else: - return reverse('helpdesk:dashboard') + return reverse("helpdesk:dashboard") @helpdesk_staff_member_required @@ -1217,12 +1302,12 @@ def raw_details(request, type_): # in the future it needs to be expanded to include other items. All it # does is return a plain-text representation of an object. - if type_ not in ('preset',): + if type_ not in ("preset",): raise Http404 - if type_ == 'preset' and request.GET.get('id', False): + if type_ == "preset" and request.GET.get("id", False): try: - preset = PreSetReply.objects.get(id=request.GET.get('id')) + preset = PreSetReply.objects.get(id=request.GET.get("id")) return HttpResponse(preset.body) except PreSetReply.DoesNotExist: raise Http404 @@ -1241,10 +1326,10 @@ def hold_ticket(request, ticket_id, unhold=False): if unhold: ticket.on_hold = False - title = _('Ticket taken off hold') + title = _("Ticket taken off hold") else: ticket.on_hold = True - title = _('Ticket placed on hold') + title = _("Ticket placed on hold") f = update_ticket( user=request.user, @@ -1273,7 +1358,7 @@ unhold_ticket = staff_member_required(unhold_ticket) @helpdesk_staff_member_required def rss_list(request): - return render(request, 'helpdesk/rss_list.html', {'queues': Queue.objects.all()}) + return render(request, "helpdesk/rss_list.html", {"queues": Queue.objects.all()}) rss_list = staff_member_required(rss_list) @@ -1282,7 +1367,7 @@ rss_list = staff_member_required(rss_list) @helpdesk_staff_member_required def report_index(request): number_tickets = Ticket.objects.all().count() - saved_query = request.GET.get('saved_query', None) + saved_query = request.GET.get("saved_query", None) user_queues = HelpdeskUser(request.user).get_queues() Tickets = Ticket.objects.filter(queue__in=user_queues) @@ -1298,22 +1383,26 @@ def report_index(request): dash_tickets = [] for queue in Queues: dash_ticket = { - 'queue': queue.id, - 'name': queue.title, - 'open': queue.ticket_set.filter(status__in=[1, 2]).count(), - 'resolved': queue.ticket_set.filter(status=3).count(), - 'closed': queue.ticket_set.filter(status=4).count(), - 'time_spent': format_time_spent(queue.time_spent), - 'dedicated_time': format_time_spent(queue.dedicated_time) + "queue": queue.id, + "name": queue.title, + "open": queue.ticket_set.filter(status__in=[1, 2]).count(), + "resolved": queue.ticket_set.filter(status=3).count(), + "closed": queue.ticket_set.filter(status=4).count(), + "time_spent": format_time_spent(queue.time_spent), + "dedicated_time": format_time_spent(queue.dedicated_time), } dash_tickets.append(dash_ticket) - return render(request, 'helpdesk/report_index.html', { - 'number_tickets': number_tickets, - 'saved_query': saved_query, - 'basic_ticket_stats': basic_ticket_stats, - 'dash_tickets': dash_tickets, - }) + return render( + request, + "helpdesk/report_index.html", + { + "number_tickets": number_tickets, + "saved_query": saved_query, + "basic_ticket_stats": basic_ticket_stats, + "dash_tickets": dash_tickets, + }, + ) report_index = staff_member_required(report_index) @@ -1328,18 +1417,20 @@ def get_report_queryset_or_redirect(request, report): "userstatus", "userpriority", "userqueue", - "daysuntilticketclosedbymonth" + "daysuntilticketclosedbymonth", ): return None, None, HttpResponseRedirect(reverse("helpdesk:report_index")) - report_queryset = Ticket.objects.all().select_related().filter( - queue__in=HelpdeskUser(request.user).get_queues() + report_queryset = ( + Ticket.objects.all() + .select_related() + .filter(queue__in=HelpdeskUser(request.user).get_queues()) ) try: saved_query, query_params = load_saved_query(request) except QueryLoadError: - return None, HttpResponseRedirect(reverse('helpdesk:report_index')) + return None, HttpResponseRedirect(reverse("helpdesk:report_index")) return report_queryset, query_params, saved_query, None @@ -1361,37 +1452,37 @@ def get_report_table_and_totals(header1, summarytable, possible_options): def update_summary_tables(report_queryset, report, summarytable, summarytable2): metric3 = False for ticket in report_queryset: - if report == 'userpriority': - metric1 = u'%s' % ticket.get_assigned_to - metric2 = u'%s' % ticket.get_priority_display() + if report == "userpriority": + metric1 = "%s" % ticket.get_assigned_to + metric2 = "%s" % ticket.get_priority_display() - elif report == 'userqueue': - metric1 = u'%s' % ticket.get_assigned_to - metric2 = u'%s' % ticket.queue.title + elif report == "userqueue": + metric1 = "%s" % ticket.get_assigned_to + metric2 = "%s" % ticket.queue.title - elif report == 'userstatus': - metric1 = u'%s' % ticket.get_assigned_to - metric2 = u'%s' % ticket.get_status_display() + elif report == "userstatus": + metric1 = "%s" % ticket.get_assigned_to + metric2 = "%s" % ticket.get_status_display() - elif report == 'usermonth': - metric1 = u'%s' % ticket.get_assigned_to - metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month) + elif report == "usermonth": + metric1 = "%s" % ticket.get_assigned_to + metric2 = "%s-%s" % (ticket.created.year, ticket.created.month) - elif report == 'queuepriority': - metric1 = u'%s' % ticket.queue.title - metric2 = u'%s' % ticket.get_priority_display() + elif report == "queuepriority": + metric1 = "%s" % ticket.queue.title + metric2 = "%s" % ticket.get_priority_display() - elif report == 'queuestatus': - metric1 = u'%s' % ticket.queue.title - metric2 = u'%s' % ticket.get_status_display() + elif report == "queuestatus": + metric1 = "%s" % ticket.queue.title + metric2 = "%s" % ticket.get_status_display() - elif report == 'queuemonth': - metric1 = u'%s' % ticket.queue.title - metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month) + elif report == "queuemonth": + metric1 = "%s" % ticket.queue.title + metric2 = "%s-%s" % (ticket.created.year, ticket.created.month) - elif report == 'daysuntilticketclosedbymonth': - metric1 = u'%s' % ticket.queue.title - metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month) + elif report == "daysuntilticketclosedbymonth": + metric1 = "%s" % ticket.queue.title + metric2 = "%s-%s" % (ticket.created.year, ticket.created.month) metric3 = ticket.modified - ticket.created metric3 = metric3.days @@ -1400,30 +1491,29 @@ def update_summary_tables(report_queryset, report, summarytable, summarytable2): summarytable[metric1, metric2] += 1 if metric3: - if report == 'daysuntilticketclosedbymonth': + if report == "daysuntilticketclosedbymonth": summarytable2[metric1, metric2] += metric3 @helpdesk_staff_member_required def run_report(request, report): - - report_queryset, query_params, saved_query, redirect = get_report_queryset_or_redirect( - request, report + report_queryset, query_params, saved_query, redirect = ( + get_report_queryset_or_redirect(request, report) ) if redirect: return redirect - if request.GET.get('saved_query', None): + if request.GET.get("saved_query", None): Query(report_queryset, query_to_base64(query_params)) summarytable = defaultdict(int) # a second table for more complex queries summarytable2 = defaultdict(int) - first_ticket = Ticket.objects.all().order_by('created')[0] + first_ticket = Ticket.objects.all().order_by("created")[0] first_month = first_ticket.created.month first_year = first_ticket.created.year - last_ticket = Ticket.objects.all().order_by('-created')[0] + last_ticket = Ticket.objects.all().order_by("-created")[0] last_month = last_ticket.created.month last_year = last_ticket.created.year @@ -1441,56 +1531,56 @@ def run_report(request, report): working = False periods.append("%s-%s" % (year, month)) - if report == 'userpriority': - title = _('User by Priority') - col1heading = _('User') + if report == "userpriority": + title = _("User by Priority") + col1heading = _("User") possible_options = [t[1].title() for t in Ticket.PRIORITY_CHOICES] - charttype = 'bar' + charttype = "bar" - elif report == 'userqueue': - title = _('User by Queue') - col1heading = _('User') + elif report == "userqueue": + title = _("User by Queue") + col1heading = _("User") queue_options = HelpdeskUser(request.user).get_queues() possible_options = [q.title for q in queue_options] - charttype = 'bar' + charttype = "bar" - elif report == 'userstatus': - title = _('User by Status') - col1heading = _('User') + elif report == "userstatus": + title = _("User by Status") + col1heading = _("User") possible_options = [s[1].title() for s in Ticket.STATUS_CHOICES] - charttype = 'bar' + charttype = "bar" - elif report == 'usermonth': - title = _('User by Month') - col1heading = _('User') + elif report == "usermonth": + title = _("User by Month") + col1heading = _("User") possible_options = periods - charttype = 'date' + charttype = "date" - elif report == 'queuepriority': - title = _('Queue by Priority') - col1heading = _('Queue') + elif report == "queuepriority": + title = _("Queue by Priority") + col1heading = _("Queue") possible_options = [t[1].title() for t in Ticket.PRIORITY_CHOICES] - charttype = 'bar' + charttype = "bar" - elif report == 'queuestatus': - title = _('Queue by Status') - col1heading = _('Queue') + elif report == "queuestatus": + title = _("Queue by Status") + col1heading = _("Queue") possible_options = [s[1].title() for s in Ticket.STATUS_CHOICES] - charttype = 'bar' + charttype = "bar" - elif report == 'queuemonth': - title = _('Queue by Month') - col1heading = _('Queue') + elif report == "queuemonth": + title = _("Queue by Month") + col1heading = _("Queue") possible_options = periods - charttype = 'date' + charttype = "date" - elif report == 'daysuntilticketclosedbymonth': - title = _('Days until ticket closed by Month') - col1heading = _('Queue') + elif report == "daysuntilticketclosedbymonth": + title = _("Days until ticket closed by Month") + col1heading = _("Queue") possible_options = periods - charttype = 'date' + charttype = "date" update_summary_tables(report_queryset, report, summarytable, summarytable2) - if report == 'daysuntilticketclosedbymonth': + if report == "daysuntilticketclosedbymonth": for key in summarytable2.keys(): summarytable[key] = summarytable2[key] / summarytable[key] @@ -1519,21 +1609,25 @@ def run_report(request, report): series_names.append(series[0]) # Add total row to table - total_data = ['Total'] + total_data = ["Total"] for hdr in possible_options: total_data.append(str(totals[hdr])) - return render(request, 'helpdesk/report_output.html', { - 'title': title, - 'charttype': charttype, - 'data': table, - 'total_data': total_data, - 'headings': column_headings, - 'series_names': series_names, - 'morrisjs_data': morrisjs_data, - 'from_saved_query': saved_query is not None, - 'saved_query': saved_query, - }) + return render( + request, + "helpdesk/report_output.html", + { + "title": title, + "charttype": charttype, + "data": table, + "total_data": total_data, + "headings": column_headings, + "series_names": series_names, + "morrisjs_data": morrisjs_data, + "from_saved_query": saved_query is not None, + "saved_query": saved_query, + }, + ) run_report = staff_member_required(run_report) @@ -1541,20 +1635,23 @@ run_report = staff_member_required(run_report) @helpdesk_staff_member_required def save_query(request): - title = request.POST.get('title', None) - shared = request.POST.get('shared', False) - if shared == 'on': # django only translates '1', 'true', 't' into True + title = request.POST.get("title", None) + shared = request.POST.get("shared", False) + if shared == "on": # django only translates '1', 'true', 't' into True shared = True - query_encoded = request.POST.get('query_encoded', None) + query_encoded = request.POST.get("query_encoded", None) if not title or not query_encoded: - return HttpResponseRedirect(reverse('helpdesk:list')) + 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)) + return HttpResponseRedirect( + "%s?saved_query=%s" % (reverse("helpdesk:list"), query.id) + ) save_query = staff_member_required(save_query) @@ -1564,21 +1661,23 @@ save_query = staff_member_required(save_query) def delete_saved_query(request, pk): query = get_object_or_404(SavedSearch, id=pk, user=request.user) - if request.method == 'POST': + if request.method == "POST": query.delete() - return HttpResponseRedirect(reverse('helpdesk:list')) + return HttpResponseRedirect(reverse("helpdesk:list")) else: - return render(request, 'helpdesk/confirm_delete_saved_query.html', {'query': query}) + return render( + request, "helpdesk/confirm_delete_saved_query.html", {"query": query} + ) delete_saved_query = staff_member_required(delete_saved_query) class EditUserSettingsView(MustBeStaffMixin, UpdateView): - template_name = 'helpdesk/user_settings.html' + template_name = "helpdesk/user_settings.html" form_class = UserSettingsForm model = UserSettings - success_url = reverse_lazy('helpdesk:dashboard') + success_url = reverse_lazy("helpdesk:dashboard") def get_object(self, queryset=None): return UserSettings.objects.get_or_create(user=self.request.user)[0] @@ -1586,9 +1685,13 @@ class EditUserSettingsView(MustBeStaffMixin, UpdateView): @helpdesk_superuser_required def email_ignore(request): - return render(request, 'helpdesk/email_ignore_list.html', { - 'ignore_list': IgnoreEmail.objects.all(), - }) + return render( + request, + "helpdesk/email_ignore_list.html", + { + "ignore_list": IgnoreEmail.objects.all(), + }, + ) email_ignore = superuser_required(email_ignore) @@ -1596,15 +1699,15 @@ email_ignore = superuser_required(email_ignore) @helpdesk_superuser_required def email_ignore_add(request): - if request.method == 'POST': + if request.method == "POST": form = EmailIgnoreForm(request.POST) if form.is_valid(): form.save() - return HttpResponseRedirect(reverse('helpdesk:email_ignore')) + return HttpResponseRedirect(reverse("helpdesk:email_ignore")) else: form = EmailIgnoreForm(request.GET) - return render(request, 'helpdesk/email_ignore_add.html', {'form': form}) + return render(request, "helpdesk/email_ignore_add.html", {"form": form}) email_ignore_add = superuser_required(email_ignore_add) @@ -1613,11 +1716,11 @@ email_ignore_add = superuser_required(email_ignore_add) @helpdesk_superuser_required def email_ignore_del(request, pk): ignore = get_object_or_404(IgnoreEmail, id=pk) - if request.method == 'POST': + if request.method == "POST": ignore.delete() - return HttpResponseRedirect(reverse('helpdesk:email_ignore')) + return HttpResponseRedirect(reverse("helpdesk:email_ignore")) else: - return render(request, 'helpdesk/email_ignore_del.html', {'ignore': ignore}) + return render(request, "helpdesk/email_ignore_del.html", {"ignore": ignore}) email_ignore_del = superuser_required(email_ignore_del) @@ -1629,10 +1732,14 @@ def ticket_cc(request, ticket_id): ticket_perm_check(request, ticket) copies_to = ticket.ticketcc_set.all() - return render(request, 'helpdesk/ticket_cc_list.html', { - 'copies_to': copies_to, - 'ticket': ticket, - }) + return render( + request, + "helpdesk/ticket_cc_list.html", + { + "copies_to": copies_to, + "ticket": ticket, + }, + ) ticket_cc = staff_member_required(ticket_cc) @@ -1644,29 +1751,35 @@ def ticket_cc_add(request, ticket_id): ticket_perm_check(request, ticket) form = None - if request.method == 'POST': + if request.method == "POST": form = TicketCCForm(request.POST) if form.is_valid(): - user = form.cleaned_data.get('user') - email = form.cleaned_data.get('email') + 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 ticketcc.save() - return HttpResponseRedirect(reverse('helpdesk:ticket_cc', kwargs={'ticket_id': ticket.id})) + return HttpResponseRedirect( + reverse("helpdesk:ticket_cc", kwargs={"ticket_id": ticket.id}) + ) - return render(request, 'helpdesk/ticket_cc_add.html', { - 'ticket': ticket, - 'form': form, - 'form_email': TicketCCEmailForm(), - 'form_user': TicketCCUserForm(), - }) + return render( + request, + "helpdesk/ticket_cc_add.html", + { + "ticket": ticket, + "form": form, + "form_email": TicketCCEmailForm(), + "form_user": TicketCCUserForm(), + }, + ) ticket_cc_add = staff_member_required(ticket_cc_add) @@ -1677,11 +1790,13 @@ def ticket_cc_del(request, ticket_id, cc_id): ticket = get_object_or_404(Ticket, id=ticket_id) cc = get_object_or_404(TicketCC, ticket__id=ticket_id, id=cc_id) - if request.method == 'POST': + if request.method == "POST": cc.delete() - return HttpResponseRedirect(reverse('helpdesk:ticket_cc', kwargs={'ticket_id': cc.ticket.id})) + return HttpResponseRedirect( + reverse("helpdesk:ticket_cc", kwargs={"ticket_id": cc.ticket.id}) + ) - return render(request, 'helpdesk/ticket_cc_del.html', {'ticket': ticket, 'cc': cc}) + return render(request, "helpdesk/ticket_cc_del.html", {"ticket": ticket, "cc": cc}) ticket_cc_del = staff_member_required(ticket_cc_del) @@ -1691,7 +1806,7 @@ ticket_cc_del = staff_member_required(ticket_cc_del) def ticket_dependency_add(request, ticket_id): ticket = get_object_or_404(Ticket, id=ticket_id) ticket_perm_check(request, ticket) - if request.method == 'POST': + if request.method == "POST": form = TicketDependencyForm(ticket, request.POST) if form.is_valid(): ticketdependency = form.save(commit=False) @@ -1701,10 +1816,14 @@ def ticket_dependency_add(request, ticket_id): return redirect(ticket.get_absolute_url()) else: form = TicketDependencyForm(ticket) - return render(request, 'helpdesk/ticket_dependency_add.html', { - 'ticket': ticket, - 'form': form, - }) + return render( + request, + "helpdesk/ticket_dependency_add.html", + { + "ticket": ticket, + "form": form, + }, + ) ticket_dependency_add = staff_member_required(ticket_dependency_add) @@ -1713,11 +1832,14 @@ 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) - if request.method == 'POST': + TicketDependency, ticket__id=ticket_id, id=dependency_id + ) + if request.method == "POST": dependency.delete() - return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket_id])) - return render(request, 'helpdesk/ticket_dependency_del.html', {'dependency': dependency}) + return HttpResponseRedirect(reverse("helpdesk:view", args=[ticket_id])) + return render( + request, "helpdesk/ticket_dependency_del.html", {"dependency": dependency} + ) ticket_dependency_del = staff_member_required(ticket_dependency_del) @@ -1727,7 +1849,7 @@ ticket_dependency_del = staff_member_required(ticket_dependency_del) def ticket_resolves_add(request, ticket_id): depends_on = get_object_or_404(Ticket, id=ticket_id) ticket_perm_check(request, depends_on) - if request.method == 'POST': + if request.method == "POST": form = TicketResolvesForm(depends_on, request.POST) if form.is_valid(): ticketdependency = form.save(commit=False) @@ -1737,24 +1859,32 @@ def ticket_resolves_add(request, ticket_id): return redirect(depends_on.get_absolute_url()) else: form = TicketResolvesForm(depends_on) - return render(request, 'helpdesk/ticket_resolves_add.html', { - 'depends_on': depends_on, - 'form': form, - }) + return render( + request, + "helpdesk/ticket_resolves_add.html", + { + "depends_on": depends_on, + "form": form, + }, + ) ticket_resolves_add = staff_member_required(ticket_resolves_add) - + @helpdesk_staff_member_required def ticket_resolves_del(request, ticket_id, dependency_id): dependency = get_object_or_404( - TicketDependency, ticket__id=ticket_id, id=dependency_id) + TicketDependency, ticket__id=ticket_id, id=dependency_id + ) depends_on_id = dependency.depends_on.id - if request.method == 'POST': + if request.method == "POST": dependency.delete() - return HttpResponseRedirect(reverse('helpdesk:view', args=[depends_on_id])) - return render(request, 'helpdesk/ticket_dependency_del.html', {'dependency': dependency}) + return HttpResponseRedirect(reverse("helpdesk:view", args=[depends_on_id])) + return render( + request, "helpdesk/ticket_dependency_del.html", {"dependency": dependency} + ) + ticket_resolves_del = staff_member_required(ticket_resolves_del) @@ -1765,13 +1895,17 @@ def attachment_del(request, ticket_id, attachment_id): ticket_perm_check(request, ticket) attachment = get_object_or_404(FollowUpAttachment, id=attachment_id) - if request.method == 'POST': + if request.method == "POST": attachment.delete() - return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket_id])) - return render(request, 'helpdesk/ticket_attachment_del.html', { - 'attachment': attachment, - 'filename': attachment.filename, - }) + return HttpResponseRedirect(reverse("helpdesk:view", args=[ticket_id])) + return render( + request, + "helpdesk/ticket_attachment_del.html", + { + "attachment": attachment, + "filename": attachment.filename, + }, + ) def calc_average_nbr_days_until_ticket_resolved(Tickets): @@ -1809,7 +1943,8 @@ def calc_basic_ticket_stats(Tickets): # >= 30 & <= 60 ota_le_60_ge_30 = all_open_tickets.filter( - created__gte=date_60_str, created__lte=date_30_str) + created__gte=date_60_str, created__lte=date_30_str + ) N_ota_le_60_ge_30 = len(ota_le_60_ge_30) # >= 60 @@ -1819,31 +1954,47 @@ def calc_basic_ticket_stats(Tickets): # (O)pen (T)icket (S)tats ots = list() # label, number entries, color, sort_string - ots.append(['Tickets < 30 days', N_ota_le_30, 'success', - sort_string(date_30_str, ''), ]) - ots.append(['Tickets 30 - 60 days', N_ota_le_60_ge_30, - 'success' if N_ota_le_60_ge_30 == 0 else 'warning', - sort_string(date_60_str, date_30_str), ]) - ots.append(['Tickets > 60 days', N_ota_ge_60, - 'success' if N_ota_ge_60 == 0 else 'danger', - sort_string('', date_60_str), ]) + ots.append( + [ + "Tickets < 30 days", + N_ota_le_30, + "success", + sort_string(date_30_str, ""), + ] + ) + ots.append( + [ + "Tickets 30 - 60 days", + N_ota_le_60_ge_30, + "success" if N_ota_le_60_ge_30 == 0 else "warning", + sort_string(date_60_str, date_30_str), + ] + ) + ots.append( + [ + "Tickets > 60 days", + N_ota_ge_60, + "success" if N_ota_ge_60 == 0 else "danger", + sort_string("", date_60_str), + ] + ) # all closed tickets - independent of user. all_closed_tickets = Tickets.filter(status=Ticket.CLOSED_STATUS) - average_nbr_days_until_ticket_closed = \ - calc_average_nbr_days_until_ticket_resolved(all_closed_tickets) + average_nbr_days_until_ticket_closed = calc_average_nbr_days_until_ticket_resolved( + all_closed_tickets + ) # all closed tickets that were opened in the last 60 days. - all_closed_last_60_days = all_closed_tickets.filter( - created__gte=date_60_str) - average_nbr_days_until_ticket_closed_last_60_days = \ + all_closed_last_60_days = all_closed_tickets.filter(created__gte=date_60_str) + average_nbr_days_until_ticket_closed_last_60_days = ( calc_average_nbr_days_until_ticket_resolved(all_closed_last_60_days) + ) # put together basic stats basic_ticket_stats = { - 'average_nbr_days_until_ticket_closed': average_nbr_days_until_ticket_closed, - 'average_nbr_days_until_ticket_closed_last_60_days': - average_nbr_days_until_ticket_closed_last_60_days, - 'open_ticket_stats': ots, + "average_nbr_days_until_ticket_closed": average_nbr_days_until_ticket_closed, + "average_nbr_days_until_ticket_closed_last_60_days": average_nbr_days_until_ticket_closed_last_60_days, + "open_ticket_stats": ots, } return basic_ticket_stats @@ -1851,11 +2002,11 @@ def calc_basic_ticket_stats(Tickets): def get_color_for_nbr_days(nbr_days): if nbr_days < 5: - color_string = 'green' + color_string = "green" elif nbr_days < 10: - color_string = 'orange' + color_string = "orange" else: # more than 10 days - color_string = 'red' + color_string = "red" return color_string @@ -1869,32 +2020,47 @@ def date_rel_to_today(today, offset): def sort_string(begin, end): - return 'sort=created&date_from=%s&date_to=%s&status=%s&status=%s&status=%s' % ( - begin, end, Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS, Ticket.RESOLVED_STATUS) + return "sort=created&date_from=%s&date_to=%s&status=%s&status=%s&status=%s" % ( + begin, + end, + Ticket.OPEN_STATUS, + Ticket.REOPENED_STATUS, + Ticket.RESOLVED_STATUS, + ) @helpdesk_staff_member_required def checklist_templates(request, checklist_template_id=None): checklist_template = None if checklist_template_id: - checklist_template = get_object_or_404(ChecklistTemplate, id=checklist_template_id) + checklist_template = get_object_or_404( + ChecklistTemplate, id=checklist_template_id + ) form = ChecklistTemplateForm(request.POST or None, instance=checklist_template) if form.is_valid(): form.save() - return redirect('helpdesk:checklist_templates') - return render(request, 'helpdesk/checklist_templates.html', { - 'checklists': ChecklistTemplate.objects.all(), - 'checklist_template': checklist_template, - 'form': form - }) + return redirect("helpdesk:checklist_templates") + return render( + request, + "helpdesk/checklist_templates.html", + { + "checklists": ChecklistTemplate.objects.all(), + "checklist_template": checklist_template, + "form": form, + }, + ) @helpdesk_staff_member_required def delete_checklist_template(request, checklist_template_id): checklist_template = get_object_or_404(ChecklistTemplate, id=checklist_template_id) - if request.method == 'POST': + if request.method == "POST": checklist_template.delete() - return redirect('helpdesk:checklist_templates') - return render(request, 'helpdesk/checklist_template_confirm_delete.html', { - 'checklist_template': checklist_template, - }) + return redirect("helpdesk:checklist_templates") + return render( + request, + "helpdesk/checklist_template_confirm_delete.html", + { + "checklist_template": checklist_template, + }, + ) diff --git a/helpdesk/webhooks.py b/helpdesk/webhooks.py index 1770e753..df70ffc4 100644 --- a/helpdesk/webhooks.py +++ b/helpdesk/webhooks.py @@ -8,6 +8,7 @@ from .signals import new_ticket_done, update_ticket_done logger = logging.getLogger(__name__) + def notify_followup_webhooks(followup): urls = settings.HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS() if not urls: @@ -15,22 +16,24 @@ def notify_followup_webhooks(followup): # Serialize the ticket associated with the followup from .serializers import TicketSerializer + ticket = followup.ticket ticket.set_custom_field_values() serialized_ticket = TicketSerializer(ticket).data # Prepare the data to send data = { - 'ticket': serialized_ticket, - 'queue_slug': ticket.queue.slug, - 'followup_id': followup.id + "ticket": serialized_ticket, + "queue_slug": ticket.queue.slug, + "followup_id": followup.id, } for url in urls: try: requests.post(url, json=data, timeout=settings.HELPDESK_WEBHOOK_TIMEOUT) except requests.exceptions.Timeout: - logger.error('Timeout while sending followup webhook to %s', url) + logger.error("Timeout while sending followup webhook to %s", url) + # listener is loaded via app.py HelpdeskConfig.ready() @receiver(update_ticket_done) @@ -44,22 +47,21 @@ def send_new_ticket_webhook(ticket): return # Serialize the ticket from .serializers import TicketSerializer + ticket.set_custom_field_values() serialized_ticket = TicketSerializer(ticket).data # Prepare the data to send - data = { - 'ticket': serialized_ticket, - 'queue_slug': ticket.queue.slug - } + data = {"ticket": serialized_ticket, "queue_slug": ticket.queue.slug} for url in urls: try: requests.post(url, json=data, timeout=settings.HELPDESK_WEBHOOK_TIMEOUT) except requests.exceptions.Timeout: - logger.error('Timeout while sending new ticket webhook to %s', url) + logger.error("Timeout while sending new ticket webhook to %s", url) + # listener is loaded via app.py HelpdeskConfig.ready() @receiver(new_ticket_done) def send_new_ticket_webhook_receiver(sender, ticket, **kwargs): - send_new_ticket_webhook(ticket) \ No newline at end of file + send_new_ticket_webhook(ticket) From aa92cb782cfa12bce573d2f8806d29a0b0e0b1ca Mon Sep 17 00:00:00 2001 From: DavidVadnais Date: Sat, 22 Mar 2025 15:37:32 -1000 Subject: [PATCH 3/6] updated pyproject.toml to modern format --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 211ea9fe..cf6e1cc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,3 @@ [tool.ruff] +[tool.ruff.lint] ignore = ["E501", "E731", "F841", "E721"] From 4c2857008eb348acae36f161eae8ffe999fc9481 Mon Sep 17 00:00:00 2001 From: DavidVadnais Date: Sat, 22 Mar 2025 15:55:22 -1000 Subject: [PATCH 4/6] drop auto tests for python3.8 since it is EOL --- .github/workflows/pythonpackage.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 444b9e25..6eb1291b 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -9,9 +9,7 @@ jobs: strategy: matrix: include: - # Explicitly include Python 3.8 and 3.9 only with Django 4 - - python-version: "3.8" - django-version: "4" + # Explicitly include Python 3.9 only with Django 4 - python-version: "3.9" django-version: "4" # Define the general matrix for Python with Django From cf46f11f4576450e232f846baa567798453d8cd2 Mon Sep 17 00:00:00 2001 From: DavidVadnais Date: Sat, 22 Mar 2025 18:43:48 -1000 Subject: [PATCH 5/6] Bump license years --- LICENSE | 2 +- README.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index d1c6237a..edaa927a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ Copyright (c) 2008 Ross Poulton (Trading as Jutda), -Copyright (c) 2008-2021 django-helpdesk contributors. +Copyright (c) 2008-2025 django-helpdesk contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/README.rst b/README.rst index 972d8c92..86a115a2 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ django-helpdesk - A Django powered ticket tracker for small businesses. .. image:: https://codecov.io/gh/django-helpdesk/django-helpdesk/branch/develop/graph/badge.svg :target: https://codecov.io/gh/django-helpdesk/django-helpdesk -Copyright 2009-2023 Ross Poulton and django-helpdesk contributors. All Rights Reserved. +Copyright 2009-2025 Ross Poulton and django-helpdesk contributors. All Rights Reserved. See LICENSE for details. django-helpdesk was formerly known as Jutda Helpdesk, named after the From 60952d0b4c51fe4cfd87d0cef3cc4f094c3c69ee Mon Sep 17 00:00:00 2001 From: DavidVadnais Date: Mon, 24 Mar 2025 08:10:01 -1000 Subject: [PATCH 6/6] Call to make checkformat in the pythonpackage.yml --- .github/workflows/pythonpackage.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 6eb1291b..355634b9 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -36,7 +36,8 @@ jobs: - name: Lint with ruff run: | pip install ruff - ruff check helpdesk + make checkformat + - name: Test with pytest run: |