mirror of
https://github.com/django-helpdesk/django-helpdesk.git
synced 2024-12-12 18:00:45 +01:00
commit
3975046590
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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'
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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> : {{ ticket.ticket }}<br>\r\n<b>Queue</b> : {{ queue.title }}<br>\r\n<b>Titre</b> : {{ ticket.title }}<br>\r\n<b>Ouvert le</b> : {{ ticket.created|date:\"l j F Y à H:i\" }}<br>\r\n<b>Soumis par</b> : {{ ticket.submitter_email|default:\"Inconnu\" }}<br>\r\n<b>Priorité</b> : {{ ticket.get_priority_display }}<br>\r\n<b>Statut</b> : {{ ticket.get_status }}<br>\r\n<b>Assigné à</b> : {{ 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 :</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> : {{ ticket.ticket }}<br>\r\n<b>Queue</b> : {{ queue.title }}<br>\r\n<b>Titre</b> : {{ ticket.title }}<br>\r\n<b>Ouvert le</b> : {{ ticket.created|date:\"l j F Y à H:i\" }}<br>\r\n<b>Soumis par</b> : {{ ticket.submitter_email|default:\"Inconnu\" }}<br>\r\n<b>Priorité</b> : {{ ticket.get_priority_display }}<br>\r\n<b>Statut</b> : {{ ticket.get_status }}<br>\r\n<b>Assigné à</b> : {{ 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 :</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> : {{ ticket.ticket }}<br>\r\n<b>Queue</b> : {{ queue.title }}<br>\r\n<b>Titre</b> : {{ ticket.title }}<br>\r\n<b>Ouvert le</b> : {{ ticket.created|date:\"l j F Y à H:i\" }}<br>\r\n<b>Soumis par</b> : {{ ticket.submitter_email|default:\"Inconnu\" }}<br>\r\n<b>Priorité</b> : {{ ticket.get_priority_display }}<br>\r\n<b>Statut</b> : {{ ticket.get_status }}<br>\r\n<b>Assigné à</b> : {{ 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 :</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> : {{ ticket.ticket }}<br>\r\n<b>Queue</b> : {{ queue.title }}<br>\r\n<b>Titre</b> : {{ ticket.title }}<br>\r\n<b>Ouvert le</b> : {{ ticket.created|date:\"l j F Y à H:i\" }}<br>\r\n<b>Soumis par</b> : {{ ticket.submitter_email|default:\"Inconnu\" }}<br>\r\n<b>Priorité</b> : {{ ticket.get_priority_display }}<br>\r\n<b>Statut</b> : {{ ticket.get_status }}<br>\r\n<b>Assigné à</b> : {{ 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 :</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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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 = {
|
||||||
|
@ -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
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
|
@ -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(
|
||||||
|
@ -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 %}
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
2
setup.py
2
setup.py
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user