Merge pull request #8 from django-helpdesk/master

Update to current
This commit is contained in:
Tom Weber 2021-02-01 10:46:26 -07:00 committed by GitHub
commit 3975046590
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 2751 additions and 1887 deletions

View File

@ -155,7 +155,7 @@ collaborative translation. If you want to help translate django-helpdesk into
languages other than English, we encourage you to make use of our Transifex languages other than English, we encourage you to make use of our Transifex
project: project:
http://www.transifex.net/projects/p/django-helpdesk/resource/core/ http://www.transifex.com/projects/p/django-helpdesk/resource/core/
Once you have translated content via Transifex, please raise an issue on the Once you have translated content via Transifex, please raise an issue on the
project Github page and tag it as "translations" to let us know it's ready to project Github page and tag it as "translations" to let us know it's ready to

View File

@ -80,6 +80,11 @@ Django project.
For further installation information see `docs/install.html` For further installation information see `docs/install.html`
and `docs/configuration.html` and `docs/configuration.html`
Testing
-------
See quicktest.py for usage details
Upgrading from previous versions Upgrading from previous versions
-------------------------------- --------------------------------
@ -111,3 +116,4 @@ We're happy to include any type of contribution! This can be:
For more information on contributing, please see the `CONTRIBUTING.rst` file. For more information on contributing, please see the `CONTRIBUTING.rst` file.
.. _note: http://docs.djangoproject.com/en/dev/ref/databases/#sqlite-string-matching .. _note: http://docs.djangoproject.com/en/dev/ref/databases/#sqlite-string-matching

View File

@ -108,8 +108,8 @@ HELPDESK_SHOW_CHANGE_PASSWORD = True
# Instead of showing the public web portal first, # Instead of showing the public web portal first,
# we can instead redirect users straight to the login page. # we can instead redirect users straight to the login page.
HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = False HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = False
LOGIN_URL = '/login/' LOGIN_URL = 'helpdesk:login'
LOGIN_REDIRECT_URL = '/login/' LOGIN_REDIRECT_URL = 'helpdesk:home'
# Database # Database
# - by default, we use SQLite3 for the demo, but you can also # - by default, we use SQLite3 for the demo, but you can also

View File

@ -13,7 +13,7 @@ project_root = os.path.dirname(here)
NAME = 'django-helpdesk-demodesk' NAME = 'django-helpdesk-demodesk'
DESCRIPTION = 'A demo Django project using django-helpdesk' DESCRIPTION = 'A demo Django project using django-helpdesk'
README = open(os.path.join(here, 'README.rst')).read() README = open(os.path.join(here, 'README.rst')).read()
VERSION = '0.3.0b2' VERSION = '0.3.0b3'
#VERSION = open(os.path.join(project_root, 'VERSION')).read().strip() #VERSION = open(os.path.join(project_root, 'VERSION')).read().strip()
AUTHOR = 'django-helpdesk team' AUTHOR = 'django-helpdesk team'
URL = 'https://github.com/django-helpdesk/django-helpdesk' URL = 'https://github.com/django-helpdesk/django-helpdesk'

View File

@ -4,42 +4,35 @@ Django Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. Copyright 2018 Timothy Hobbs. All Rights Reserved. (c) Copyright 2008 Jutda. Copyright 2018 Timothy Hobbs. All Rights Reserved.
See LICENSE for details. See LICENSE for details.
""" """
from django.core.exceptions import ValidationError # import base64
from django.core.files.base import ContentFile
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management.base import BaseCommand
from django.db.models import Q
from django.utils.translation import ugettext as _
from django.utils import encoding, timezone
from django.contrib.auth import get_user_model
from helpdesk import settings
from helpdesk.lib import safe_template_context, process_attachments
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, IgnoreEmail
from datetime import timedelta
import base64
import binascii
import email import email
from email.header import decode_header
from email.utils import getaddresses, parseaddr, collapse_rfc2231_value
import imaplib import imaplib
import logging
import mimetypes import mimetypes
from os import listdir, unlink import os
from os.path import isfile, join
import poplib import poplib
import re import re
import socket import socket
import ssl import ssl
import sys import sys
from datetime import timedelta
from email.utils import getaddresses
from os.path import isfile, join
from time import ctime from time import ctime
from optparse import make_option
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models import Q
from django.utils import encoding, timezone
from django.utils.translation import ugettext as _
from email_reply_parser import EmailReplyParser from email_reply_parser import EmailReplyParser
import logging from helpdesk import settings
from helpdesk.lib import safe_template_context, process_attachments
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, IgnoreEmail
# import User model, which may be a custom model # import User model, which may be a custom model
User = get_user_model() User = get_user_model()
@ -70,37 +63,48 @@ def process_email(quiet=False):
if q.logging_type in logging_types: if q.logging_type in logging_types:
logger.setLevel(logging_types[q.logging_type]) 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':
logging.disable(logging.CRITICAL) # disable all messages # disable all handlers so messages go to nowhere
logger.handlers = []
logger.propagate = False
if quiet: 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
logdir = q.logging_dir or '/var/log/helpdesk/'
# Log messages to specific file only if the queue has it configured
if (q.logging_type in logging_types) and q.logging_dir: # if it's enabled and the dir is set
log_file_handler = logging.FileHandler(join(q.logging_dir, q.slug + '_get_email.log'))
logger.addHandler(log_file_handler)
else:
log_file_handler = None
try: try:
handler = logging.FileHandler(join(logdir, q.slug + '_get_email.log'))
logger.addHandler(handler)
if not q.email_box_last_check: if not q.email_box_last_check:
q.email_box_last_check = timezone.now() - timedelta(minutes=30) q.email_box_last_check = timezone.now() - timedelta(minutes=30)
queue_time_delta = timedelta(minutes=q.email_box_interval or 0) queue_time_delta = timedelta(minutes=q.email_box_interval or 0)
if (q.email_box_last_check + queue_time_delta) < timezone.now(): if (q.email_box_last_check + queue_time_delta) < timezone.now():
process_queue(q, logger=logger) process_queue(q, logger=logger)
q.email_box_last_check = timezone.now() q.email_box_last_check = timezone.now()
q.save() q.save()
finally: finally:
# we must close the file handler correctly if it's created
try: try:
handler.close() if log_file_handler:
log_file_handler.close()
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
try: try:
logger.removeHandler(handler) if log_file_handler:
logger.removeHandler(log_file_handler)
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
def pop3_sync(q, logger, server): def pop3_sync(q, logger, server):
server.getwelcome() server.getwelcome()
try:
server.stls()
except Exception:
logger.warning("POP3 StartTLS failed or unsupported. Connection will be unencrypted.")
server.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER) server.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER)
server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD) server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD)
@ -138,17 +142,27 @@ def pop3_sync(q, logger, server):
def imap_sync(q, logger, server): def imap_sync(q, logger, server):
try: try:
try:
server.starttl()
except Exception:
logger.warning("IMAP4 StartTLS unsupported or failed. Connection will be unencrypted.")
server.login(q.email_box_user or server.login(q.email_box_user or
settings.QUEUE_EMAIL_BOX_USER, settings.QUEUE_EMAIL_BOX_USER,
q.email_box_pass or q.email_box_pass or
settings.QUEUE_EMAIL_BOX_PASSWORD) settings.QUEUE_EMAIL_BOX_PASSWORD)
server.select(q.email_box_imap_folder) server.select(q.email_box_imap_folder)
except imaplib.IMAP4.abort: except imaplib.IMAP4.abort:
logger.error("IMAP login failed. Check that the server is accessible and that the username and password are correct.") logger.error(
"IMAP login failed. Check that the server is accessible and that "
"the username and password are correct."
)
server.logout() server.logout()
sys.exit() sys.exit()
except ssl.SSLError: except ssl.SSLError:
logger.error("IMAP login failed due to SSL error. This is often due to a timeout. Please check your connection and try again.") logger.error(
"IMAP login failed due to SSL error. This is often due to a timeout. "
"Please check your connection and try again."
)
server.logout() server.logout()
sys.exit() sys.exit()
@ -171,7 +185,10 @@ def imap_sync(q, logger, server):
else: else:
logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % num) logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % num)
except imaplib.IMAP4.error: 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) logger.error(
"IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?",
q.email_box_imap_folder
)
server.expunge() server.expunge()
server.close() server.close()
@ -243,7 +260,7 @@ def process_queue(q, logger):
elif email_box_type == 'local': elif email_box_type == 'local':
mail_dir = q.email_box_local_dir or '/var/lib/mail/helpdesk/' mail_dir = q.email_box_local_dir or '/var/lib/mail/helpdesk/'
mail = [join(mail_dir, f) for f in listdir(mail_dir) if isfile(join(mail_dir, f))] mail = [join(mail_dir, f) for f in os.listdir(mail_dir) if isfile(join(mail_dir, f))]
logger.info("Found %d messages in local mailbox directory" % len(mail)) logger.info("Found %d messages in local mailbox directory" % len(mail))
logger.info("Found %d messages in local mailbox directory" % len(mail)) logger.info("Found %d messages in local mailbox directory" % len(mail))
@ -253,15 +270,15 @@ def process_queue(q, logger):
full_message = encoding.force_text(f.read(), errors='replace') full_message = encoding.force_text(f.read(), errors='replace')
ticket = object_from_message(message=full_message, queue=q, logger=logger) ticket = object_from_message(message=full_message, queue=q, logger=logger)
if ticket: if ticket:
logger.info("Successfully processed message %d, ticket/comment created." % i) logger.info("Successfully processed message %d, ticket/comment created.", i)
try: try:
unlink(m) # delete message file if ticket was successful os.unlink(m) # delete message file if ticket was successful
except OSError: except OSError as e:
logger.error("Unable to delete message %d." % i) logger.error("Unable to delete message %d (%s).", i, str(e))
else: else:
logger.info("Successfully deleted message %d." % i) logger.info("Successfully deleted message %d.", i)
else: else:
logger.warn("Message %d was not successfully processed, and will be left in local directory" % i) logger.warn("Message %d was not successfully processed, and will be left in local directory", i)
def decodeUnknown(charset, string): def decodeUnknown(charset, string):
@ -277,7 +294,11 @@ def decodeUnknown(charset, string):
def decode_mail_headers(string): def decode_mail_headers(string):
decoded = email.header.decode_header(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 u' '.join([
str(msg, encoding=charset, errors='replace') if charset else str(msg)
for msg, charset
in decoded
])
def create_ticket_cc(ticket, cc_list): def create_ticket_cc(ticket, cc_list):
@ -305,7 +326,7 @@ def create_ticket_cc(ticket, cc_list):
try: try:
ticket_cc = subscribe_to_ticket_updates(ticket=ticket, user=user, email=cced_email) ticket_cc = subscribe_to_ticket_updates(ticket=ticket, user=user, email=cced_email)
new_ticket_ccs.append(ticket_cc) new_ticket_ccs.append(ticket_cc)
except ValidationError as err: except ValidationError:
pass pass
return new_ticket_ccs return new_ticket_ccs
@ -362,7 +383,6 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
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 new = True
update = ''
# Old issue being re-opened # Old issue being re-opened
elif ticket.status == Ticket.CLOSED_STATUS: elif ticket.status == Ticket.CLOSED_STATUS:
@ -389,7 +409,10 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
attached = process_attachments(f, files) attached = process_attachments(f, files)
for att_file in attached: 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)) logger.info(
"Attachment '%s' (with size %s) successfully added to ticket from email.",
att_file[0], att_file[1].size
)
context = safe_template_context(ticket) context = safe_template_context(ticket)
@ -402,8 +425,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
ticket_cc_list = TicketCC.objects.filter(ticket=ticket).all().values_list('email', flat=True) ticket_cc_list = TicketCC.objects.filter(ticket=ticket).all().values_list('email', flat=True)
for email in ticket_cc_list: for email_address in ticket_cc_list:
notifications_to_be_sent.append(email) notifications_to_be_sent.append(email_address)
# send mail to appropriate people now depending on what objects # send mail to appropriate people now depending on what objects
# were created and who was CC'd # were created and who was CC'd
@ -453,8 +476,6 @@ def object_from_message(message, queue, logger):
# correctly. Not ideal, but this seems to work for now. # correctly. Not ideal, but this seems to work for now.
sender_email = email.utils.getaddresses(['\"' + sender.replace('<', '\" <')])[0][1] sender_email = email.utils.getaddresses(['\"' + sender.replace('<', '\" <')])[0][1]
body_plain, body_html = '', ''
cc = message.get_all('cc', None) cc = message.get_all('cc', None)
if cc: if cc:
# first, fixup the encoding if necessary # first, fixup the encoding if necessary
@ -530,18 +551,23 @@ def object_from_message(message, queue, logger):
if not name: if not name:
ext = mimetypes.guess_extension(part.get_content_type()) ext = mimetypes.guess_extension(part.get_content_type())
name = "part-%i%s" % (counter, ext) name = "part-%i%s" % (counter, ext)
# FIXME: this code gets the paylods, then does something with it and then completely ignores it
# writing the part.get_payload(decode=True) instead; and then the payload variable is
# replaced by some dict later.
# the `payloadToWrite` has been also ignored so was commented
payload = part.get_payload() payload = part.get_payload()
if isinstance(payload, list): if isinstance(payload, list):
payload = payload.pop().as_string() payload = payload.pop().as_string()
payloadToWrite = payload # payloadToWrite = payload
# check version of python to ensure use of only the correct error type # check version of python to ensure use of only the correct error type
non_b64_err = TypeError non_b64_err = TypeError
try: try:
logger.debug("Try to base64 decode the attachment payload") logger.debug("Try to base64 decode the attachment payload")
payloadToWrite = base64.decodebytes(payload) # payloadToWrite = base64.decodebytes(payload)
except non_b64_err: except non_b64_err:
logger.debug("Payload was not base64 encoded, using raw bytes") logger.debug("Payload was not base64 encoded, using raw bytes")
payloadToWrite = payload # payloadToWrite = payload
files.append(SimpleUploadedFile(name, part.get_payload(decode=True), mimetypes.guess_type(name)[0])) files.append(SimpleUploadedFile(name, part.get_payload(decode=True), mimetypes.guess_type(name)[0]))
logger.debug("Found MIME attachment %s" % name) logger.debug("Found MIME attachment %s" % name)

View File

@ -739,7 +739,7 @@
"heading" : "Ticket mis à jour", "heading" : "Ticket mis à jour",
"subject" : "(Mis à jour)", "subject" : "(Mis à jour)",
"template_name" : "updated_cc", "template_name" : "updated_cc",
"html" : "<p style=\"font-family: sans-serif; font-size: 1em;\">Bonjour,</p>\r\n\r\n<p style=\"font-family: sans-serif; font-size: 1em;\">Ce courriel indicatif permet de vous prévenir que le ticket {{ ticket.ticket }} (\"{{ ticket.title }}\") par {{ ticket.submitter_email }} a été mis à jour.</p>\r\n\r\n<p style=\"font-family: sans-serif; font-size: 1em;\">\r\n<b>File d'attente</b>&nbsp;: {{ ticket.ticket }}<br>\r\n<b>Queue</b>&nbsp;: {{ queue.title }}<br>\r\n<b>Titre</b>&nbsp;: {{ ticket.title }}<br>\r\n<b>Ouvert le</b>&nbsp;: {{ ticket.created|date:\"l j F Y à H:i\" }}<br>\r\n<b>Soumis par</b>&nbsp;: {{ ticket.submitter_email|default:\"Inconnu\" }}<br>\r\n<b>Priorité</b>&nbsp;: {{ ticket.get_priority_display }}<br>\r\n<b>Statut</b>&nbsp;: {{ ticket.get_status }}<br>\r\n<b>Assigné à</b>&nbsp;: {{ ticket.get_assigned_to }}<br>\r\n<b><a href='{{ ticket.staff_url }}'>Voir le ticket en ligne</a></b> pour le mettre à jour (après authentification)</p>\r\n\r\n<p style=\"font-family: sans-serif; font-size: 1em;\">Pour mémoire, la description originelle était&nbsp;:</p>\r\n\r\n<blockquote style=\"font-family: sans-serif; font-size: 1em;\">{{ ticket.description|linebreaksbr }}</blockquote>\r\n\r\n<p style=\"font-family: sans-serif; font-size: 1em;\">Le commentaire suivant a été ajouté :</p>\r\n\r\n<blockquote style=\"font-family: sans-serif; font-size: 1em;\">{{ comment }}</blockquote>\r\n\r\n<p style=\"font-family: Tahoma, Arial, sans-serif; font-size: 11pt;\">Cette information{% if private %}n' a pas{% else %}a{% endif %}été envoyé par mail à l'émetteur.</p>" "html" : "<p style=\"font-family: sans-serif; font-size: 1em;\">Bonjour,</p>\r\n\r\n<p style=\"font-family: sans-serif; font-size: 1em;\">Ce courriel indicatif permet de vous prévenir que le ticket {{ ticket.ticket }} (\"{{ ticket.title }}\") par {{ ticket.submitter_email }} a été mis à jour.</p>\r\n\r\n<p style=\"font-family: sans-serif; font-size: 1em;\">\r\n<b>File d'attente</b>&nbsp;: {{ ticket.ticket }}<br>\r\n<b>Queue</b>&nbsp;: {{ queue.title }}<br>\r\n<b>Titre</b>&nbsp;: {{ ticket.title }}<br>\r\n<b>Ouvert le</b>&nbsp;: {{ ticket.created|date:\"l j F Y à H:i\" }}<br>\r\n<b>Soumis par</b>&nbsp;: {{ ticket.submitter_email|default:\"Inconnu\" }}<br>\r\n<b>Priorité</b>&nbsp;: {{ ticket.get_priority_display }}<br>\r\n<b>Statut</b>&nbsp;: {{ ticket.get_status }}<br>\r\n<b>Assigné à</b>&nbsp;: {{ ticket.get_assigned_to }}<br>\r\n<b><a href='{{ ticket.staff_url }}'>Voir le ticket en ligne</a></b> pour le mettre à jour (après authentification)</p>\r\n\r\n<p style=\"font-family: sans-serif; font-size: 1em;\">Pour mémoire, la description originelle était&nbsp;:</p>\r\n\r\n<blockquote style=\"font-family: sans-serif; font-size: 1em;\">{{ ticket.description|linebreaksbr }}</blockquote>\r\n\r\n<p style=\"font-family: sans-serif; font-size: 1em;\">Le commentaire suivant a été ajouté :</p>\r\n\r\n<blockquote style=\"font-family: sans-serif; font-size: 1em;\">{{ comment }}</blockquote>\r\n\r\n<p style=\"font-family: Tahoma, Arial, sans-serif; font-size: 11pt;\">Cette information {% if private %}n' a pas{% else %}a{% endif %} été envoyé par mail à l'émetteur.</p>"
}, },
"pk" : 62 "pk" : 62
}, },
@ -750,8 +750,8 @@
"heading" : "Ticket mis à jour", "heading" : "Ticket mis à jour",
"template_name" : "updated_owner", "template_name" : "updated_owner",
"subject" : "(Mis à jour - à vous)", "subject" : "(Mis à jour - à vous)",
"html" : "<p style=\"font-family: sans-serif; font-size: 1em;\">Bonjour,</p>\r\n\r\n<p style=\"font-family: sans-serif; font-size: 1em;\">Ce courriel indicatif permet de vous prévenir que le ticket {{ ticket.ticket }} (\"{{ ticket.title }}\") par {{ ticket.submitter_email }}, qui vous est assigné, a été mis à jour.</p>\r\n\r\n<p style=\"font-family: sans-serif; font-size: 1em;\">\r\n<b>File d'attente</b>&nbsp;: {{ ticket.ticket }}<br>\r\n<b>Queue</b>&nbsp;: {{ queue.title }}<br>\r\n<b>Titre</b>&nbsp;: {{ ticket.title }}<br>\r\n<b>Ouvert le</b>&nbsp;: {{ ticket.created|date:\"l j F Y à H:i\" }}<br>\r\n<b>Soumis par</b>&nbsp;: {{ ticket.submitter_email|default:\"Inconnu\" }}<br>\r\n<b>Priorité</b>&nbsp;: {{ ticket.get_priority_display }}<br>\r\n<b>Statut</b>&nbsp;: {{ ticket.get_status }}<br>\r\n<b>Assigné à</b>&nbsp;: {{ ticket.get_assigned_to }}<br>\r\n<b><a href='{{ ticket.staff_url }}'>Voir le ticket en ligne</a></b> pour le mettre à jour (après authentification)</p>\r\n\r\n<p style=\"font-family: sans-serif; font-size: 1em;\">Pour mémoire, la description originelle était&nbsp;:</p>\r\n\r\n<blockquote style=\"font-family: sans-serif; font-size: 1em;\">{{ ticket.description|linebreaksbr }}</blockquote>\r\n\r\n<p style=\"font-family: sans-serif; font-size: 1em;\">Le commentaire suivant a été ajouté :</p>\r\n\r\n<blockquote style=\"font-family: sans-serif; font-size: 1em;\">{{ comment }}</blockquote>\r\n\r\n<p style=\"font-family: Tahoma, Arial, sans-serif; font-size: 11pt;\">Cette information{% if private %}n' a pas{% else %}a{% endif %}été envoyé par mail à l'émetteur.</p>", "html" : "<p style=\"font-family: sans-serif; font-size: 1em;\">Bonjour,</p>\r\n\r\n<p style=\"font-family: sans-serif; font-size: 1em;\">Ce courriel indicatif permet de vous prévenir que le ticket {{ ticket.ticket }} (\"{{ ticket.title }}\") par {{ ticket.submitter_email }}, qui vous est assigné, a été mis à jour.</p>\r\n\r\n<p style=\"font-family: sans-serif; font-size: 1em;\">\r\n<b>File d'attente</b>&nbsp;: {{ ticket.ticket }}<br>\r\n<b>Queue</b>&nbsp;: {{ queue.title }}<br>\r\n<b>Titre</b>&nbsp;: {{ ticket.title }}<br>\r\n<b>Ouvert le</b>&nbsp;: {{ ticket.created|date:\"l j F Y à H:i\" }}<br>\r\n<b>Soumis par</b>&nbsp;: {{ ticket.submitter_email|default:\"Inconnu\" }}<br>\r\n<b>Priorité</b>&nbsp;: {{ ticket.get_priority_display }}<br>\r\n<b>Statut</b>&nbsp;: {{ ticket.get_status }}<br>\r\n<b>Assigné à</b>&nbsp;: {{ ticket.get_assigned_to }}<br>\r\n<b><a href='{{ ticket.staff_url }}'>Voir le ticket en ligne</a></b> pour le mettre à jour (après authentification)</p>\r\n\r\n<p style=\"font-family: sans-serif; font-size: 1em;\">Pour mémoire, la description originelle était&nbsp;:</p>\r\n\r\n<blockquote style=\"font-family: sans-serif; font-size: 1em;\">{{ ticket.description|linebreaksbr }}</blockquote>\r\n\r\n<p style=\"font-family: sans-serif; font-size: 1em;\">Le commentaire suivant a été ajouté :</p>\r\n\r\n<blockquote style=\"font-family: sans-serif; font-size: 1em;\">{{ comment }}</blockquote>\r\n\r\n<p style=\"font-family: Tahoma, Arial, sans-serif; font-size: 11pt;\">Cette information {% if private %}n' a pas{% else %}a{% endif %} été envoyé par mail à l'émetteur.</p>",
"plain_text" : "Hello,\r\n\r\nCe courriel indicatif permet de vous prévenir que le ticket {{ ticket.ticket }} (\"{{ ticket.title }}\") par {{ ticket.submitter_email }}, qui vous est assigné, a été mis à jour.\r\n\r\nIdentifiant : {{ ticket.ticket }}\r\nFile d'attente : {{ queue.title }}\r\nTitre : {{ ticket.title }}\r\nOuvert le : {{ ticket.created|date:\"l j F Y à H:i\" }}\r\nSoumis par : {{ ticket.submitter_email|default:\"Inconnu\" }}\r\nPriorité : {{ ticket.get_priority_display }}\r\nStatut : {{ ticket.get_status }}\r\nAssigné à : {{ ticket.get_assigned_to }}\r\nAdresse : {{ ticket.staff_url }}\r\n\r\nDescription originelle :\r\n\r\n{{ ticket.description }}\r\n\r\nLe commentaire suivant a été ajouté :\r\n\r\n{{ comment }}\r\n\r\nCette information{% if private %}n' a pas{% else %}a{% endif %}été envoyé par mail à l'émetteur.\r\n\r\n", "plain_text" : "Hello,\r\n\r\nCe courriel indicatif permet de vous prévenir que le ticket {{ ticket.ticket }} (\"{{ ticket.title }}\") par {{ ticket.submitter_email }}, qui vous est assigné, a été mis à jour.\r\n\r\nIdentifiant : {{ ticket.ticket }}\r\nFile d'attente : {{ queue.title }}\r\nTitre : {{ ticket.title }}\r\nOuvert le : {{ ticket.created|date:\"l j F Y à H:i\" }}\r\nSoumis par : {{ ticket.submitter_email|default:\"Inconnu\" }}\r\nPriorité : {{ ticket.get_priority_display }}\r\nStatut : {{ ticket.get_status }}\r\nAssigné à : {{ ticket.get_assigned_to }}\r\nAdresse : {{ ticket.staff_url }}\r\n\r\nDescription originelle :\r\n\r\n{{ ticket.description }}\r\n\r\nLe commentaire suivant a été ajouté :\r\n\r\n{{ comment }}\r\n\r\nCette information {% if private %}n' a pas{% else %}a{% endif %} été envoyé par mail à l'émetteur.\r\n\r\n",
"locale" : "fr" "locale" : "fr"
} }
}, },

View File

@ -98,6 +98,9 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
try: try:
current_value = TicketCustomFieldValue.objects.get(ticket=self.instance, field=field) current_value = TicketCustomFieldValue.objects.get(ticket=self.instance, field=field)
initial_value = current_value.value initial_value = current_value.value
# If it is boolean field, transform the value to a real boolean instead of a string
if current_value.field.data_type == 'boolean':
initial_value = initial_value == 'True'
except TicketCustomFieldValue.DoesNotExist: except TicketCustomFieldValue.DoesNotExist:
initial_value = None initial_value = None
instanceargs = { instanceargs = {

View File

@ -502,7 +502,7 @@ msgstr "Složka pro log soubory"
#: third_party/django-helpdesk/helpdesk/models.py:306 #: third_party/django-helpdesk/helpdesk/models.py:306
msgid "" msgid ""
"If logging is enabled, what directory should we use to store log files for " "If logging is enabled, what directory should we use to store log files for "
"this queue? If no directory is set, default to /var/log/helpdesk/" "this queue? The standard logging mechanims are used if no directory is set"
msgstr "" msgstr ""
#: third_party/django-helpdesk/helpdesk/models.py:317 #: third_party/django-helpdesk/helpdesk/models.py:317

File diff suppressed because it is too large Load Diff

View File

@ -522,11 +522,10 @@ msgstr "Dossier de logs"
#: .\models.py:308 #: .\models.py:308
msgid "" msgid ""
"If logging is enabled, what directory should we use to store log files for " "If logging is enabled, what directory should we use to store log files for "
"this queue? If no directory is set, default to /var/log/helpdesk/" "this queue? The standard logging mechanims are used if no directory is set"
msgstr "" msgstr ""
"Si les logs sont activés, quel dossier doit être utilisé pour stocker les " "Si les logs sont activés, quel dossier doit être utilisé pour stocker les "
"fichiers de logs pour cette file ? Si aucun dossier n'est défini, cela sera /" "fichiers de logs pour cette file?"
"var/log/helpdesk/ par défaut"
#: .\models.py:319 #: .\models.py:319
msgid "Default owner" msgid "Default owner"

View File

@ -531,13 +531,15 @@ msgstr ""
#: models.py:247 #: models.py:247
msgid "Logging Directory" msgid "Logging Directory"
msgstr "" msgstr "Директория логов"
#: models.py:251 #: models.py:251
msgid "" msgid ""
"If logging is enabled, what directory should we use to store log files for " "If logging is enabled, what directory should we use to store log files for "
"this queue? If no directory is set, default to /var/log/helpdesk/" "this queue? The standard logging mechanims are used if no directory is set"
msgstr "" msgstr ""
"Директория в которую будут сохраняться файлы с логами; стандартная конфигурация "
"используется если ничего не указано"
#: models.py:261 #: models.py:261
msgid "Default owner" msgid "Default owner"

View File

@ -470,7 +470,7 @@ msgstr ""
#: models.py:308 #: models.py:308
msgid "" msgid ""
"If logging is enabled, what directory should we use to store log files for " "If logging is enabled, what directory should we use to store log files for "
"this queue? If no directory is set, default to /var/log/helpdesk/" "this queue? The standard logging mechanims are used if no directory is set"
msgstr "" msgstr ""
#: models.py:319 #: models.py:319

View File

@ -18,7 +18,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='queue', model_name='queue',
name='logging_dir', 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? If no directory is set, default to /var/log/helpdesk/', max_length=200, null=True, verbose_name='Logging Directory'), 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( migrations.AddField(
model_name='queue', model_name='queue',

View File

@ -307,7 +307,7 @@ class Queue(models.Model):
null=True, null=True,
help_text=_('If logging is enabled, what directory should we use to ' help_text=_('If logging is enabled, what directory should we use to '
'store log files for this queue? ' 'store log files for this queue? '
'If no directory is set, default to /var/log/helpdesk/'), 'The standard logging mechanims are used if no directory is set'),
) )
default_owner = models.ForeignKey( default_owner = models.ForeignKey(

View File

@ -34,7 +34,7 @@
{% else %} {% else %}
<div class="form-group"> <div class="form-group">
<label for='id_{{ field.name }}'> <label for='id_{{ field.name }}'>
{% trans field.label %} {{ field.label }}
{% if not field.field.required %} {% if not field.field.required %}
({% trans "Optional" %}) ({% trans "Optional" %})
{% endif %} {% endif %}

View File

@ -153,9 +153,10 @@ class GetEmailParametricTemplate(object):
else: else:
# Test local email reading # Test local email reading
if self.method == 'local': if self.method == 'local':
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \ with mock.patch('os.listdir') as mocked_listdir, \
mock.patch('helpdesk.email.isfile') as mocked_isfile, \ mock.patch('helpdesk.email.isfile') as mocked_isfile, \
mock.patch('builtins.open', mock.mock_open(read_data=test_email)): mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \
mock.patch('os.unlink'):
mocked_isfile.return_value = True mocked_isfile.return_value = True
mocked_listdir.return_value = ['filename1', 'filename2'] mocked_listdir.return_value = ['filename1', 'filename2']
@ -224,9 +225,10 @@ class GetEmailParametricTemplate(object):
else: else:
# Test local email reading # Test local email reading
if self.method == 'local': if self.method == 'local':
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \ with mock.patch('os.listdir') as mocked_listdir, \
mock.patch('helpdesk.email.isfile') as mocked_isfile, \ mock.patch('helpdesk.email.isfile') as mocked_isfile, \
mock.patch('builtins.open' if six.PY3 else '__builtin__.open', mock.mock_open(read_data=test_email)): mock.patch('builtins.open' if six.PY3 else '__builtin__.open', mock.mock_open(read_data=test_email)), \
mock.patch('os.unlink'):
mocked_isfile.return_value = True mocked_isfile.return_value = True
mocked_listdir.return_value = ['filename1', 'filename2'] mocked_listdir.return_value = ['filename1', 'filename2']
@ -299,9 +301,10 @@ class GetEmailParametricTemplate(object):
else: else:
# Test local email reading # Test local email reading
if self.method == 'local': if self.method == 'local':
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \ with mock.patch('os.listdir') as mocked_listdir, \
mock.patch('helpdesk.email.isfile') as mocked_isfile, \ mock.patch('helpdesk.email.isfile') as mocked_isfile, \
mock.patch('builtins.open', mock.mock_open(read_data=test_email)): mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \
mock.patch('os.unlink'):
mocked_isfile.return_value = True mocked_isfile.return_value = True
mocked_listdir.return_value = ['filename1', 'filename2'] mocked_listdir.return_value = ['filename1', 'filename2']
@ -412,9 +415,10 @@ class GetEmailParametricTemplate(object):
else: else:
# Test local email reading # Test local email reading
if self.method == 'local': if self.method == 'local':
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \ with mock.patch('os.listdir') as mocked_listdir, \
mock.patch('helpdesk.email.isfile') as mocked_isfile, \ mock.patch('helpdesk.email.isfile') as mocked_isfile, \
mock.patch('builtins.open', mock.mock_open(read_data=msg.as_string())): mock.patch('builtins.open', mock.mock_open(read_data=msg.as_string())), \
mock.patch('os.unlink'):
mocked_isfile.return_value = True mocked_isfile.return_value = True
mocked_listdir.return_value = ['filename1', 'filename2'] mocked_listdir.return_value = ['filename1', 'filename2']
@ -502,9 +506,10 @@ class GetEmailParametricTemplate(object):
else: else:
# Test local email reading # Test local email reading
if self.method == 'local': if self.method == 'local':
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \ with mock.patch('os.listdir') as mocked_listdir, \
mock.patch('helpdesk.email.isfile') as mocked_isfile, \ mock.patch('helpdesk.email.isfile') as mocked_isfile, \
mock.patch('builtins.open', mock.mock_open(read_data=test_email)): mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \
mock.patch('os.unlink'):
mocked_isfile.return_value = True mocked_isfile.return_value = True
mocked_listdir.return_value = ['filename1'] mocked_listdir.return_value = ['filename1']
@ -670,9 +675,10 @@ class GetEmailCCHandling(TestCase):
test_email = "To: queue@example.com\nCc: " + test_email_cc_one + ", " + test_email_cc_one + ", " + test_email_cc_two + ", " + test_email_cc_three + "\nCC: " + test_email_cc_one + ", " + test_email_cc_three + ", " + test_email_cc_four + ", " + ticket_user_emails + "\nFrom: " + test_email_from + "\nSubject: " + test_email_subject + "\n\n" + test_email_body test_email = "To: queue@example.com\nCc: " + test_email_cc_one + ", " + test_email_cc_one + ", " + test_email_cc_two + ", " + test_email_cc_three + "\nCC: " + test_email_cc_one + ", " + test_email_cc_three + ", " + test_email_cc_four + ", " + ticket_user_emails + "\nFrom: " + test_email_from + "\nSubject: " + test_email_subject + "\n\n" + test_email_body
test_mail_len = len(test_email) test_mail_len = len(test_email)
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \ with mock.patch('os.listdir') as mocked_listdir, \
mock.patch('helpdesk.email.isfile') as mocked_isfile, \ mock.patch('helpdesk.email.isfile') as mocked_isfile, \
mock.patch('builtins.open', mock.mock_open(read_data=test_email)): mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \
mock.patch('os.unlink'):
mocked_isfile.return_value = True mocked_isfile.return_value = True
mocked_listdir.return_value = ['filename1'] mocked_listdir.return_value = ['filename1']
@ -708,7 +714,10 @@ class GetEmailCCHandling(TestCase):
# build matrix of test cases # 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]
case_socks = [False] + [c[0] for c in Queue._meta.get_field('socks_proxy_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]
case_matrix = list(itertools.product(case_methods, case_socks)) case_matrix = list(itertools.product(case_methods, case_socks))
# Populate TestCases from the matrix of parameters # Populate TestCases from the matrix of parameters

View File

@ -1,3 +1,10 @@
"""
Usage:
$ python -m venv .venv
$ source .venv/bin/activate
$ pip install -r requirements-testing.txt -r requirements.txt
$ python ./quicktest.py
"""
import os import os
import sys import sys
import argparse import argparse
@ -65,7 +72,6 @@ class QuickDjangoTest(object):
}, },
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.tests = args self.tests = args
self._tests() self._tests()
@ -102,6 +108,7 @@ class QuickDjangoTest(object):
if failures: if failures:
sys.exit(failures) sys.exit(failures)
if __name__ == '__main__': if __name__ == '__main__':
""" """
What do when the user hits this file from the shell. What do when the user hits this file from the shell.

View File

@ -6,7 +6,7 @@ from distutils.util import convert_path
from fnmatch import fnmatchcase from fnmatch import fnmatchcase
from setuptools import setup, find_packages from setuptools import setup, find_packages
version = '0.3.0b2' version = '0.3.0b3'
# Provided as an attribute, so you can append to these instead # Provided as an attribute, so you can append to these instead
# of replicating them: # of replicating them: