Merge branch 'release/add_user_threading_feature'

This commit is contained in:
Bruno Tikami 2016-02-20 18:01:36 -02:00
commit 71a0fdfc9c
9 changed files with 696 additions and 135 deletions

View File

@ -28,7 +28,8 @@ Dependencies (pre-flight checklist)
1. Python 2.7 or 3.4+ (3.4+ support is new, please let us know how it goes)
2. Django (1.7 or newer, preferably 1.9 - Django 1.7 is not supported if you are using Python 3.5)
3. An existing WORKING Django project with database etc. If you
cannot log into the Admin, you won't get this product working.
cannot log into the Admin, you won't get this product working. Also, make sure that both
"django.contrib.humanize" and "django.contrib.sites" are installed.
4. `pip install django-bootstrap-form` and add `bootstrapform` to `settings.INSTALLED_APPS`
5. `pip install django-markdown-deux` and add `markdown_deux` to `settings.INSTALLED_APPS`
6. `pip install email-reply-parser` to get smart email reply handling

View File

@ -540,6 +540,16 @@ class TicketCCForm(forms.ModelForm):
model = TicketCC
exclude = ('ticket',)
def clean(self):
cleaned_data = super(TicketCCForm, self).clean()
user = cleaned_data.get('user', None)
email = cleaned_data.get('email', '')
if user is None and len(email) == 0:
raise forms.ValidationError(_('When you add somebody on Cc, you must provided either an User or a valid email.'))
class TicketDependencyForm(forms.ModelForm):
class Meta:
model = TicketDependency

View File

@ -22,7 +22,7 @@ logger = logging.getLogger('helpdesk')
from django.utils.encoding import smart_str
def send_templated_mail(template_name, email_context, recipients, sender=None, bcc=None, fail_silently=False, files=None):
def send_templated_mail(template_name, email_context, recipients, sender=None, bcc=None, fail_silently=False, files=None, extra_headers={}):
"""
send_templated_mail() is a warpper around Django's e-mail routines that
allows us to easily send multipart (text/plain & text/html) e-mails using
@ -133,7 +133,9 @@ def send_templated_mail(template_name, email_context, recipients, sender=None, b
text_part,
sender,
recipients,
bcc=bcc)
bcc=bcc,
headers=extra_headers,
)
msg.attach_alternative(html_part, "text/html")
if files:

View File

@ -36,7 +36,7 @@ except ImportError:
from datetime import datetime as timezone
from helpdesk.lib import send_templated_mail, safe_template_context
from helpdesk.models import Queue, Ticket, FollowUp, Attachment, IgnoreEmail
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, Attachment, IgnoreEmail
class Command(BaseCommand):
@ -124,7 +124,7 @@ def process_queue(q, quiet=False):
msgSize = msg.split(" ")[1]
full_message = "\n".join(server.retr(msgNum)[1])
ticket = ticket_from_message(message=full_message, queue=q, quiet=quiet)
ticket = object_from_message(message=full_message, queue=q, quiet=quiet)
if ticket:
server.dele(msgNum)
@ -147,7 +147,7 @@ def process_queue(q, quiet=False):
msgnums = data[0].split()
for num in msgnums:
status, data = server.fetch(num, '(RFC822)')
ticket = ticket_from_message(message=data[0][1], queue=q, quiet=quiet)
ticket = object_from_message(message=data[0][1], queue=q, quiet=quiet)
if ticket:
server.store(num, '+FLAGS', '\\Deleted')
@ -168,10 +168,207 @@ def decode_mail_headers(string):
decoded = decode_header(string)
return u' '.join([unicode(msg, charset or 'utf-8') for msg, charset in decoded])
def ticket_from_message(message, queue, quiet):
def create_ticket_cc(ticket, cc_list):
# Local import to deal with non-defined / circular reference problem
from helpdesk.views.staff import User, subscribe_to_ticket_updates
new_ticket_ccs = []
for cced_email in cc_list:
user = None
try:
user = User.objects.get(email=cced_email)
except User.DoesNotExist:
pass
ticket_cc = subscribe_to_ticket_updates(ticket=ticket, user=user, email=cced_email)
new_ticket_ccs.append(ticket_cc)
return new_ticket_ccs
def create_object_from_email_message(message, ticket_id, payload, files, quiet):
ticket, previous_followup, new = None, None, False
now = timezone.now()
queue = payload['queue']
sender_email = payload['sender_email']
message_id = message.get('Message-Id')
in_reply_to = message.get('In-Reply-To')
cc_list = message.get('Cc')
if in_reply_to is not None:
try:
queryset = FollowUp.objects.filter(message_id=in_reply_to).order_by('-date')
if queryset.count() > 0:
previous_followup = queryset.first()
ticket = previous_followup.ticket
except FollowUp.DoesNotExist:
pass #play along. The header may be wrong
if previous_followup is None and ticket_id is not None:
try:
ticket = Ticket.objects.get(id=ticket_id)
new = False
except Ticket.DoesNotExist:
ticket = None
# New issue, create a new <Ticket> instance
if ticket is None:
ticket = Ticket.objects.create(
title = payload['subject'],
queue = queue,
submitter_email = sender_email,
created = now,
description = payload['body'],
priority = payload['priority'],
)
ticket.save()
new = True
update = ''
# Old issue being re-openned
elif ticket.status == Ticket.CLOSED_STATUS:
ticket.status = Ticket.REOPENED_STATUS
ticket.save()
f = FollowUp(
ticket = ticket,
title = _('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}),
date = now,
public = True,
comment = payload['body'],
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.save()
if not quiet:
print (" [%s-%s] %s" % (ticket.queue.slug, ticket.id, ticket.title,)).encode('ascii', 'replace')
for file in files:
if file['content']:
filename = file['filename'].encode('ascii', 'replace').replace(' ', '_')
filename = re.sub('[^a-zA-Z0-9._-]+', '', filename)
a = Attachment(
followup=f,
filename=filename,
mime_type=file['type'],
size=len(file['content']),
)
a.file.save(filename, ContentFile(file['content']), save=False)
a.save()
if not quiet:
print " - %s" % filename
context = safe_template_context(ticket)
new_ticket_ccs = []
if cc_list is not None:
new_ticket_ccs = create_ticket_cc(ticket, cc_list.split(','))
notification_template = None
if new:
notification_template = 'newticket_cc'
if sender_email:
send_templated_mail(
'newticket_submitter',
context,
recipients=sender_email,
sender=queue.from_address,
fail_silently=True,
extra_headers={'In-Reply-To': message_id},
)
if queue.new_ticket_cc:
send_templated_mail(
'newticket_cc',
context,
recipients=queue.new_ticket_cc,
sender=queue.from_address,
fail_silently=True,
extra_headers={'In-Reply-To': message_id},
)
if queue.updated_ticket_cc and queue.updated_ticket_cc != queue.new_ticket_cc:
send_templated_mail(
'newticket_cc',
context,
recipients=queue.updated_ticket_cc,
sender=queue.from_address,
fail_silently=True,
extra_headers={'In-Reply-To': message_id},
)
else:
notification_template = 'updated_cc'
context.update(comment=f.comment)
if ticket.status == Ticket.REOPENED_STATUS:
update = _(' (Reopened)')
else:
update = _(' (Updated)')
if ticket.assigned_to:
send_templated_mail(
'updated_owner',
context,
recipients=ticket.assigned_to.email,
sender=queue.from_address,
fail_silently=True,
)
if queue.updated_ticket_cc:
send_templated_mail(
'updated_cc',
context,
recipients=queue.updated_ticket_cc,
sender=queue.from_address,
fail_silently=True,
)
notifications_to_be_sent = []
ticket_cc_list = TicketCC.objects.filter(ticket=ticket).all().values_list('email', flat=True)
for email in ticket_cc_list :
notifications_to_be_sent.append(email)
if len(notifications_to_be_sent):
send_templated_mail(
notification_template,
context,
recipients=notifications_to_be_sent,
sender=queue.from_address,
fail_silently=True,
extra_headers={'In-Reply-To': message_id},
)
return ticket
def object_from_message(message, queue, quiet):
# 'message' must be an RFC822 formatted message.
msg = message
message = email.message_from_string(msg)
subject = message.get('subject', _('Created from e-mail'))
subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject))
subject = subject.replace("Re: ", "").replace("Fw: ", "").replace("RE: ", "").replace("FW: ", "").replace("Automatic reply: ", "").strip()
@ -194,9 +391,9 @@ def ticket_from_message(message, queue, quiet):
matchobj = re.match(r".*\["+queue.slug+"-(?P<id>\d+)\]", subject)
if matchobj:
# This is a reply or forward.
ticket = matchobj.group('id')
ticket_id = matchobj.group('id')
else:
ticket = None
ticket_id = None
counter = 0
files = []
@ -239,14 +436,7 @@ def ticket_from_message(message, queue, quiet):
'type': 'text/html',
})
now = timezone.now()
if ticket:
try:
t = Ticket.objects.get(id=ticket)
new = False
except Ticket.DoesNotExist:
ticket = None
priority = 3
@ -258,115 +448,17 @@ def ticket_from_message(message, queue, quiet):
if smtp_priority in high_priority_types or smtp_importance in high_priority_types:
priority = 2
if ticket == None:
t = Ticket(
title=subject,
queue=queue,
submitter_email=sender_email,
created=now,
description=body,
priority=priority,
)
t.save()
new = True
update = ''
elif t.status == Ticket.CLOSED_STATUS:
t.status = Ticket.REOPENED_STATUS
t.save()
f = FollowUp(
ticket = t,
title = _('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}),
date = timezone.now(),
public = True,
comment = body,
)
if t.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.save()
if not quiet:
print (" [%s-%s] %s" % (t.queue.slug, t.id, t.title,)).encode('ascii', 'replace')
for file in files:
if file['content']:
filename = file['filename'].encode('ascii', 'replace').replace(' ', '_')
filename = re.sub('[^a-zA-Z0-9._-]+', '', filename)
a = Attachment(
followup=f,
filename=filename,
mime_type=file['type'],
size=len(file['content']),
)
a.file.save(filename, ContentFile(file['content']), save=False)
a.save()
if not quiet:
print " - %s" % filename
payload = {
'body': body,
'subject': subject,
'queue': queue,
'sender_email': sender_email,
'priority': priority,
'files': files,
}
context = safe_template_context(t)
if new:
if sender_email:
send_templated_mail(
'newticket_submitter',
context,
recipients=sender_email,
sender=queue.from_address,
fail_silently=True,
)
if queue.new_ticket_cc:
send_templated_mail(
'newticket_cc',
context,
recipients=queue.new_ticket_cc,
sender=queue.from_address,
fail_silently=True,
)
if queue.updated_ticket_cc and queue.updated_ticket_cc != queue.new_ticket_cc:
send_templated_mail(
'newticket_cc',
context,
recipients=queue.updated_ticket_cc,
sender=queue.from_address,
fail_silently=True,
)
else:
context.update(comment=f.comment)
if t.status == Ticket.REOPENED_STATUS:
update = _(' (Reopened)')
else:
update = _(' (Updated)')
if t.assigned_to:
send_templated_mail(
'updated_owner',
context,
recipients=t.assigned_to.email,
sender=queue.from_address,
fail_silently=True,
)
if queue.updated_ticket_cc:
send_templated_mail(
'updated_cc',
context,
recipients=queue.updated_ticket_cc,
sender=queue.from_address,
fail_silently=True,
)
return t
return create_object_from_email_message(message, ticket_id, payload, files, quiet=quiet)
if __name__ == '__main__':
process_email()

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.1 on 2016-02-07 19:51
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0011_admin_related_improvements'),
]
operations = [
migrations.AddField(
model_name='ticket',
name='submitter_email_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='Submitter E-Mail ID'),
),
]

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.1 on 2016-02-16 18:13
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0012_add_submitter_email_id_field_to_ticket'),
]
operations = [
migrations.RemoveField(
model_name='ticket',
name='submitter_email_id',
),
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'),
),
]

View File

@ -623,6 +623,15 @@ class FollowUp(models.Model):
help_text=_('If the status was changed, what was it changed to?'),
)
message_id = models.CharField(
_('E-Mail ID'),
max_length=256,
blank=True,
null=True,
help_text=_("The Message ID of the submitter's email."),
editable=False,
)
objects = FollowUpManager()
class Meta:

View File

@ -1,9 +1,17 @@
from helpdesk.models import Queue, CustomField, Ticket
import email
import uuid
from helpdesk.models import Queue, CustomField, FollowUp, Ticket, TicketCC
from django.test import TestCase
from django.core import mail
from django.core.exceptions import ObjectDoesNotExist
from django.forms import ValidationError
from django.test.client import Client
from django.core.urlresolvers import reverse
from helpdesk.management.commands.get_email import object_from_message, create_ticket_cc
try: # python 3
from urllib.parse import urlparse
except ImportError: # python 2
@ -24,13 +32,385 @@ class TicketBasicsTestCase(TestCase):
self.client = Client()
def test_create_ticket_direct(self):
def test_create_ticket_instance_from_payload(self):
"""
Ensure that a <Ticket> instance is created whenever an email is sent to a public queue.
"""
email_count = len(mail.outbox)
ticket_data = dict(queue=self.queue_public, **self.ticket_data)
ticket = Ticket.objects.create(**ticket_data)
self.assertEqual(ticket.ticket_for_url, "q1-%s" % ticket.id)
self.assertEqual(email_count, len(mail.outbox))
def test_create_ticket_from_email_with_message_id(self):
"""
Ensure that a <Ticket> instance is created whenever an email is sent to a public queue.
Also, make sure that the RFC 2822 field "message-id" is stored on the <Ticket.submitter_email_id>
field.
"""
msg = email.message.Message()
message_id = uuid.uuid4().hex
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'])
email_count = len(mail.outbox)
#print email_count
#for m in mail.outbox:
# print m.to, m.subject
object_from_message(str(msg), self.queue_public, quiet=True)
followup = FollowUp.objects.get(message_id=message_id)
ticket = Ticket.objects.get(id=followup.ticket.id)
self.assertEqual(ticket.ticket_for_url, "q1-%s" % ticket.id)
# As we have created an Ticket from an email, we notify the sender (+1)
# and the new and update queues (+2)
self.assertEqual(email_count + 1 + 2, len(mail.outbox))
def test_create_ticket_from_email_without_message_id(self):
"""
Ensure that a <Ticket> instance is created whenever an email is sent to a public queue.
Also, make sure that the RFC 2822 field "message-id" is stored on the <Ticket.submitter_email_id>
field.
"""
msg = email.message.Message()
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'])
email_count = len(mail.outbox)
object_from_message(str(msg), self.queue_public, quiet=True)
ticket = Ticket.objects.get(title=self.ticket_data['title'], queue=self.queue_public, submitter_email=submitter_email)
self.assertEqual(ticket.ticket_for_url, "q1-%s" % ticket.id)
# As we have created an Ticket from an email, we notify the sender (+1)
# and the new and update queues (+2)
self.assertEqual(email_count + 1 + 2, len(mail.outbox))
def test_create_ticket_from_email_with_carbon_copy(self):
"""
Ensure that an instance of <TicketCC> is created for every valid element of the
"rfc_2822_cc" field when creating a <Ticket> instance.
"""
msg = email.message.Message()
message_id = uuid.uuid4().hex
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'])
email_count = len(mail.outbox)
object_from_message(str(msg), self.queue_public, quiet=True)
followup = FollowUp.objects.get(message_id=message_id)
ticket = Ticket.objects.get(id=followup.ticket.id)
self.assertEqual(ticket.ticket_for_url, "q1-%s" % ticket.id)
# As we have created an Ticket from an email, we notify the sender (+1),
# the new and update queues (+2) and contacts on the cc_list (+1 as it's
# treated as a list)
self.assertEqual(email_count + 1 + 2 + 1, len(mail.outbox))
# Ensure that <TicketCC> is created
for cc_email in cc_list:
ticket_cc = TicketCC.objects.get(ticket=ticket, email=cc_email)
self.assertTrue(ticket_cc.ticket, ticket)
self.assertTrue(ticket_cc.email, cc_email)
def test_create_ticket_from_email_with_invalid_carbon_copy(self):
"""
Ensure that no <TicketCC> instance is created if an invalid element of the
"rfc_2822_cc" field is provided when creating a <Ticket> instance.
"""
msg = email.message.Message()
message_id = uuid.uuid4().hex
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'])
email_count = len(mail.outbox)
self.assertRaises(ValidationError, object_from_message, str(msg), self.queue_public, quiet=True)
def test_create_followup_from_email_with_valid_message_id_with_when_no_initial_cc_list(self):
"""
Ensure that if a message is received with an valid In-Reply-To ID,
the expected <TicketCC> instances are created even if the there were
no <TicketCC>s so far.
"""
### Ticket and TicketCCs creation ###
msg = email.message.Message()
message_id = uuid.uuid4().hex
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'])
email_count = len(mail.outbox)
object_from_message(str(msg), self.queue_public, quiet=True)
followup = FollowUp.objects.get(message_id=message_id)
ticket = Ticket.objects.get(id=followup.ticket.id)
### end of the Ticket and TicketCCs creation ###
# Reply message
reply = email.message.Message()
reply_message_id = uuid.uuid4().hex
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'])
object_from_message(str(reply), self.queue_public, quiet=True)
followup = FollowUp.objects.get(message_id=message_id)
ticket = Ticket.objects.get(id=followup.ticket.id)
self.assertEqual(ticket.ticket_for_url, "q1-%s" % ticket.id)
# Ensure that <TicketCC> is created
for cc_email in cc_list:
# Even after 2 messages with the same cc_list, <get> MUST return only
# one object
ticket_cc = TicketCC.objects.get(ticket=ticket, email=cc_email)
self.assertTrue(ticket_cc.ticket, ticket)
self.assertTrue(ticket_cc.email, cc_email)
# As we have created an Ticket from an email, we notify the sender (+1)
# and the new and update queues (+2)
expected_email_count = 1 + 2
# As an update was made, we increase the expected_email_count with:
# cc_list: +1
# public_update_queue: +1
expected_email_count += 1 + 1
self.assertEqual(expected_email_count, len(mail.outbox))
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 <TicketCC> instances are created but if there's any
overlap with the previous Cc list, no duplicates are created.
"""
### 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']
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)
object_from_message(str(msg), self.queue_public, quiet=True)
followup = FollowUp.objects.get(message_id=message_id)
ticket = Ticket.objects.get(id=followup.ticket.id)
# Ensure that <TicketCC> is created
for cc_email in cc_list:
ticket_cc = TicketCC.objects.get(ticket=ticket, email=cc_email)
self.assertTrue(ticket_cc.ticket, ticket)
self.assertTrue(ticket_cc.email, cc_email)
self.assertTrue(ticket_cc.can_view, True)
# As we have created an Ticket from an email, we notify the sender (+1),
# the new and update queues (+2) and contacts on the cc_list (+1 as it's
# treated as a list)
self.assertEqual(email_count + 1 + 2 + 1, len(mail.outbox))
### end of the Ticket and TicketCCs creation ###
# Reply message
reply = email.message.Message()
reply_message_id = uuid.uuid4().hex
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'])
object_from_message(str(reply), self.queue_public, quiet=True)
followup = FollowUp.objects.get(message_id=message_id)
ticket = Ticket.objects.get(id=followup.ticket.id)
self.assertEqual(ticket.ticket_for_url, "q1-%s" % ticket.id)
# Ensure that <TicketCC> is created
for cc_email in cc_list:
# Even after 2 messages with the same cc_list,
# <get> MUST return only one object
ticket_cc = TicketCC.objects.get(ticket=ticket, email=cc_email)
self.assertTrue(ticket_cc.ticket, ticket)
self.assertTrue(ticket_cc.email, cc_email)
# As we have created an Ticket from an email, we notify the sender (+1),
# the new and update queues (+2) and contacts on the cc_list (+1 as it's
# treated as a list)
expected_email_count = 1 + 2 + 1
# As an update was made, we increase the expected_email_count with:
# cc_list: +1
# public_update_queue: +1
expected_email_count += 1 + 1
self.assertEqual(expected_email_count, len(mail.outbox))
def test_create_followup_from_email_with_invalid_message_id(self):
"""
Ensure that if a message is received with an invalid In-Reply-To ID and we
can infer the original Ticket ID by the message's subject, the expected
<TicketCC> instances are created
"""
### 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']
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)
object_from_message(str(msg), self.queue_public, quiet=True)
followup = FollowUp.objects.get(message_id=message_id)
ticket = Ticket.objects.get(id=followup.ticket.id)
# Ensure that <TicketCC> is created
for cc_email in cc_list:
ticket_cc = TicketCC.objects.get(ticket=ticket, email=cc_email)
self.assertTrue(ticket_cc.ticket, ticket)
self.assertTrue(ticket_cc.email, cc_email)
self.assertTrue(ticket_cc.can_view, True)
# As we have created an Ticket from an email, we notify the sender (+1),
# the new and update queues (+2) and contacts on the cc_list (+1 as it's
# treated as a list)
self.assertEqual(email_count + 1 + 2 + 1, len(mail.outbox))
### end of the Ticket and TicketCCs creation ###
# Reply message
reply = email.message.Message()
reply_message_id = uuid.uuid4().hex
submitter_email = 'foo@bar.py'
cc_list = ['bravo@example.net', 'charlie@foobar.com']
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'])
email_count = len(mail.outbox)
object_from_message(str(reply), self.queue_public, quiet=True)
followup = FollowUp.objects.get(message_id=message_id)
ticket = Ticket.objects.get(id=followup.ticket.id)
self.assertEqual(ticket.ticket_for_url, "q1-%s" % ticket.id)
# Ensure that <TicketCC> is created
for cc_email in cc_list:
# Even after 2 messages with the same cc_list, <get> MUST return only
# one object
ticket_cc = TicketCC.objects.get(ticket=ticket, email=cc_email)
self.assertTrue(ticket_cc.ticket, ticket)
self.assertTrue(ticket_cc.email, cc_email)
# As we have created an Ticket from an email, we notify the sender (+1),
# the new and update queues (+2) and contacts on the cc_list (+1 as it's
# treated as a list)
self.assertEqual(email_count + 1 + 2 + 1, len(mail.outbox))
def test_create_ticket_public(self):
email_count = len(mail.outbox)

View File

@ -338,15 +338,38 @@ def return_ticketccstring_and_show_subscribe(user, ticket):
return ticketcc_string, SHOW_SUBSCRIBE
def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, can_update=False):
def subscribe_staff_member_to_ticket(ticket, user):
''' used in view_ticket() and update_ticket() '''
ticketcc = TicketCC()
data = {
'user': user,
'email': email,
'can_view': can_view,
'can_update': can_update
}
ticket_cc_form = TicketCCForm(data)
if ticket is not None and ticket_cc_form.is_valid():
queryset = TicketCC.objects.filter(ticket=ticket, user=user, email=email)
if queryset.count() > 0:
return queryset.first()
ticketcc = ticket_cc_form.save(commit=False)
ticketcc.ticket = ticket
ticketcc.user = user
ticketcc.can_view = True
ticketcc.can_update = True
ticketcc.save()
return ticketcc
else:
raise ValidationError(
_('Could not create subscribe contact to ticket updated. Errors: {}'.format(ticket_cc_form.errors))
)
def subscribe_staff_member_to_ticket(ticket, user, email=''):
''' used in view_ticket() and update_ticket() '''
return subscribe_to_ticket_updates(ticket=ticket, user=user, email=email, can_view=can_view, can_update=can_update)
def update_ticket(request, ticket_id, public=False):