From 4d00dd3d6ec9a07bb613bdcf1197a7f215fc1bae Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 7 Feb 2016 12:06:07 -0200 Subject: [PATCH 01/30] UPDATED: Renamed test to so the new upcoming test names (including CC, In-Reply etc) are easier to understand. --- helpdesk/tests/test_ticket_submission.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index e5f9933b..c91d12c1 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -24,13 +24,12 @@ class TicketBasicsTestCase(TestCase): self.client = Client() - def test_create_ticket_direct(self): + def test_create_ticket_from_email(self): 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_public(self): email_count = len(mail.outbox) From 703c0e3b669c7ff5895f23ba0a60ee1f064df8dd Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 7 Feb 2016 15:49:48 -0200 Subject: [PATCH 02/30] ADDED: and to ensure instances are created by the model when the respective RFC 2822 field is provided. --- helpdesk/tests/test_ticket_submission.py | 72 +++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index c91d12c1..1a129e58 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -1,6 +1,10 @@ -from helpdesk.models import Queue, CustomField, Ticket + +import uuid + +from helpdesk.models import Queue, CustomField, Ticket, TicketCC from django.test import TestCase from django.core import mail +from django.core.exceptions import ObjectDoesNotExist from django.test.client import Client from django.core.urlresolvers import reverse @@ -31,6 +35,72 @@ class TicketBasicsTestCase(TestCase): self.assertEqual(ticket.ticket_for_url, "q1-%s" % ticket.id) self.assertEqual(email_count, len(mail.outbox)) + def test_create_ticket_from_email_with_carbon_copy(self): + + """ + Ensure that an instance of is created for every valid element of the + "rfc_2822_cc" field when creating a instance. + """ + + message_id = uuid.uuid4().hex + + email_data = { + 'Message-ID': message_id, + 'cc': ['bravo@example.net', 'charlie@foobar.com'], + } + + # Regular ticket from email creation process + self.ticket_data = { + 'title': 'Test Ticket', + 'description': 'Some Test Ticket', + 'rfc_2822_cc': email_data.get('cc', []) + } + + 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)) + + # Ensure that is created + for cc_email in email_data.get('cc', []): + + 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 instance is created if an invalid element of the + "rfc_2822_cc" field is provided when creating a instance. + """ + + message_id = uuid.uuid4().hex + + email_data = { + 'Message-ID': message_id, + 'cc': ['null@example', 'invalid@foobar'], + } + + # Regular ticket from email creation process + self.ticket_data = { + 'title': 'Test Ticket', + 'description': 'Some Test Ticket', + 'rfc_2822_cc': email_data.get('cc', []) + } + + 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)) + + # Ensure that is created + for cc_email in email_data.get('cc', []): + + self.assertEquals(0, TicketCC.objects.filter(ticket=ticket, email=cc_email).count()) + def test_create_ticket_public(self): email_count = len(mail.outbox) From 353fcb2138a9df8291989ab745e5adbc0b2508eb Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 7 Feb 2016 15:54:39 -0200 Subject: [PATCH 03/30] ADDED: Possibility to pass "rfc_2822_*" fields when creating a instance so can create instances when processing incoming messages. --- helpdesk/models.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/helpdesk/models.py b/helpdesk/models.py index 6069f6cc..fcb0cb03 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -428,6 +428,24 @@ class Ticket(models.Model): 'automatically by management/commands/escalate_tickets.py.'), ) + def __init__(self, *args, **kwargs): + + # Separate RFC 2822 (email) exclusive fields for later processing + self.rfc_2822_items = {} + + for field, value in kwargs.iteritems(): + if field.startswith('rfc_2822'): + self.rfc_2822_items[field] = value + + # Submitter Message-Id is an exception here, since it's a attribute + if 'rfc_2822_message_id' in kwargs: + kwargs['submitter_message_id'] = value + + for field in self.rfc_2822_items.iterkeys(): + kwargs.pop(field) + + super(Ticket, self).__init__(*args, **kwargs) + def _get_assigned_to(self): """ Custom property to allow us to easily print 'Unassigned' if a ticket has no owner, or the users name if it's assigned. If the user @@ -542,6 +560,27 @@ class Ticket(models.Model): return ('helpdesk_view', (self.id,)) get_absolute_url = models.permalink(get_absolute_url) + def process_rfc_2822_data(self): + + if len(self.rfc_2822_items) > 0: + cc_list = self.rfc_2822_items.get('rfc_2822_cc', []) + + for cced_email in cc_list: + + user = None + + user_model = get_user_model() + + try: + user = user_model.objects.get(email=cced_email) + except user_model.DoesNotExist: + pass + + from .views import staff + + ticket_cc = staff.subscribe_staff_member_to_ticket(ticket=self, user=user, email=cced_email) + + def save(self, *args, **kwargs): if not self.id: # This is a new ticket as no ID yet exists. @@ -554,6 +593,9 @@ class Ticket(models.Model): super(Ticket, self).save(*args, **kwargs) + # Process RFC 2822 fields, if any + self.process_rfc_2822_data() + class FollowUpManager(models.Manager): def private_followups(self): From b5ff3f90637143a2aa0a3bfbd79c72d4e5ddfec0 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 7 Feb 2016 15:56:30 -0200 Subject: [PATCH 04/30] UPDATED: to accept and validate an user email when creating linking a instance for a staff member. I'm pretty sure this is NOT the best place to put this but I don't wanna mess too much around for now. --- helpdesk/views/staff.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 2a00f090..3aaac599 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -22,6 +22,7 @@ from django.contrib.auth.decorators import login_required, user_passes_test from django.core.files.base import ContentFile from django.core.urlresolvers import reverse from django.core.exceptions import ValidationError, PermissionDenied +from django.core.validators import EmailValidator from django.core import paginator from django.db import connection from django.db.models import Q @@ -338,16 +339,29 @@ def return_ticketccstring_and_show_subscribe(user, ticket): return ticketcc_string, SHOW_SUBSCRIBE +def subscribe_to_ticket_updates(ticket, user=None, email=''): + + try: + EmailValidator()(email) + except ValidationError: + email = '' -def subscribe_staff_member_to_ticket(ticket, user): - ''' used in view_ticket() and update_ticket() ''' ticketcc = TicketCC() ticketcc.ticket = ticket ticketcc.user = user + ticketcc.email = email ticketcc.can_view = True ticketcc.can_update = True ticketcc.save() + return ticketcc + +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) + def update_ticket(request, ticket_id, public=False): if not (public or (request.user.is_authenticated() and request.user.is_active and (request.user.is_staff or helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE))): From e880747a2b021bae8acfc35f2914d915fb23fb6f Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 7 Feb 2016 16:46:43 -0200 Subject: [PATCH 05/30] UDPATED: Use instead of when adding non-staff members to instances --- helpdesk/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helpdesk/models.py b/helpdesk/models.py index fcb0cb03..2726b36f 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -576,9 +576,10 @@ class Ticket(models.Model): except user_model.DoesNotExist: pass + # Local import to deal with non-defined / circular reference problem from .views import staff - ticket_cc = staff.subscribe_staff_member_to_ticket(ticket=self, user=user, email=cced_email) + ticket_cc = staff.subscribe_to_ticket_updates(ticket=self, user=user, email=cced_email) def save(self, *args, **kwargs): From ff0ceefab4c2f70d5c4079b8ab6b89f1ba377eee Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 7 Feb 2016 16:54:05 -0200 Subject: [PATCH 06/30] ADDED: to ensure that the RFC 2822 field "message-id" is stored on the field. Backwards-compatible test kept. --- helpdesk/tests/test_ticket_submission.py | 27 +++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index 1a129e58..9a2b36ea 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -28,13 +28,38 @@ class TicketBasicsTestCase(TestCase): self.client = Client() - def test_create_ticket_from_email(self): + def test_create_ticket_from_email_without_message_id(self): + + """ + Ensure that a instance is created whenever an email is sent to a public queue. + """ + 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 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 + field. + """ + + message_id = uuid.uuid4().hex + + self.ticket_data['rfc_2822_message-id'] = message_id + + 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)) + self.assertEqual(ticket.submitter_email_id, message_id) + + def test_create_ticket_from_email_with_carbon_copy(self): """ From 83be21dc176e889810e1af90099a951c2c18c205 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 7 Feb 2016 17:28:00 -0200 Subject: [PATCH 07/30] ADDED: --- helpdesk/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/helpdesk/models.py b/helpdesk/models.py index 2726b36f..f2d13dbe 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -370,6 +370,14 @@ class Ticket(models.Model): 'follow-ups left for this task.'), ) + submitter_email_id = models.CharField( + _('Submitter E-Mail ID'), + blank=True, + null=True, + help_text=_("The Message ID of the submitter's email."), + editable=False, + ) + assigned_to = models.ForeignKey( settings.AUTH_USER_MODEL, related_name='assigned_to', From 78919addd769e49ac77cca4471843e541c8e5ff0 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 7 Feb 2016 17:45:08 -0200 Subject: [PATCH 08/30] BUGFIX: Set the missing max_length field attribute. --- helpdesk/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/helpdesk/models.py b/helpdesk/models.py index f2d13dbe..df4b1711 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -372,6 +372,7 @@ class Ticket(models.Model): submitter_email_id = models.CharField( _('Submitter E-Mail ID'), + max_length=256, blank=True, null=True, help_text=_("The Message ID of the submitter's email."), From b615bc6a54d14aadea4bc6611d2b5f81933cb8e9 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 7 Feb 2016 17:58:53 -0200 Subject: [PATCH 09/30] ADDED: Schema migration 0012 that adds a "submitter_message_id" to the model --- ...d_submitter_email_id_field_to_ticket.py.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py.py diff --git a/helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py.py b/helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py.py new file mode 100644 index 00000000..beafe939 --- /dev/null +++ b/helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py.py @@ -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'), + ), + ] From 880003743d3dcce0a77bc6bbf798d055772cebe3 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 7 Feb 2016 17:58:53 -0200 Subject: [PATCH 10/30] ADDED: Schema migration 0012 that adds a "submitter_message_id" to the model --- ..._add_submitter_email_id_field_to_ticket.py | 20 +++++++++++++++++++ ...d_submitter_email_id_field_to_ticket.py.py | 20 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py create mode 100644 helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py.py diff --git a/helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py b/helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py new file mode 100644 index 00000000..beafe939 --- /dev/null +++ b/helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py @@ -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'), + ), + ] diff --git a/helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py.py b/helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py.py new file mode 100644 index 00000000..beafe939 --- /dev/null +++ b/helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py.py @@ -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'), + ), + ] From ea3c3732e5177e4560754b4dab225de98caad7cb Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 7 Feb 2016 18:05:16 -0200 Subject: [PATCH 11/30] BUGFIX: File name typo --- ...d_submitter_email_id_field_to_ticket.py.py | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py.py diff --git a/helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py.py b/helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py.py deleted file mode 100644 index beafe939..00000000 --- a/helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- 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'), - ), - ] From cea4cb2c6a4d8279b830c274484828eeedf2f9e5 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 7 Feb 2016 18:11:49 -0200 Subject: [PATCH 12/30] UPDATED: Set "submitter_email_id" as the standard to reference the Message-Id email field. --- helpdesk/models.py | 4 ++-- helpdesk/tests/test_ticket_submission.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/helpdesk/models.py b/helpdesk/models.py index df4b1711..d18614ed 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -447,8 +447,8 @@ class Ticket(models.Model): self.rfc_2822_items[field] = value # Submitter Message-Id is an exception here, since it's a attribute - if 'rfc_2822_message_id' in kwargs: - kwargs['submitter_message_id'] = value + if 'rfc_2822_submitter_email_id' in kwargs: + kwargs['submitter_email_id'] = kwargs['rfc_2822_submitter_email_id'] for field in self.rfc_2822_items.iterkeys(): kwargs.pop(field) diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index 9a2b36ea..821d6043 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -50,7 +50,7 @@ class TicketBasicsTestCase(TestCase): message_id = uuid.uuid4().hex - self.ticket_data['rfc_2822_message-id'] = message_id + self.ticket_data['rfc_2822_submitter_email_id'] = message_id email_count = len(mail.outbox) ticket_data = dict(queue=self.queue_public, **self.ticket_data) From 5b46602b293dd429f479c97b1d4a375a306c3a06 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Mon, 15 Feb 2016 16:15:46 -0200 Subject: [PATCH 13/30] UPDATED: now calls the right callable to ensure the message is parsed and the Ticket instance is created. --- helpdesk/tests/test_ticket_submission.py | 28 ++++++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index 821d6043..e33b3f89 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -1,4 +1,5 @@ +import email import uuid from helpdesk.models import Queue, CustomField, Ticket, TicketCC @@ -8,6 +9,8 @@ from django.core.exceptions import ObjectDoesNotExist from django.test.client import Client from django.core.urlresolvers import reverse +from helpdesk.management.commands.get_email import ticket_from_message + try: # python 3 from urllib.parse import urlparse except ImportError: # python 2 @@ -28,7 +31,7 @@ class TicketBasicsTestCase(TestCase): self.client = Client() - def test_create_ticket_from_email_without_message_id(self): + def test_create_ticket_instance_from_payload(self): """ Ensure that a instance is created whenever an email is sent to a public queue. @@ -48,16 +51,27 @@ class TicketBasicsTestCase(TestCase): field. """ - message_id = uuid.uuid4().hex + msg = email.message.Message() - self.ticket_data['rfc_2822_submitter_email_id'] = message_id + 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) - ticket_data = dict(queue=self.queue_public, **self.ticket_data) - ticket = Ticket.objects.create(**ticket_data) + + ticket_from_message(str(msg), self.queue_public, quiet=True) + ticket = Ticket.objects.get(title=self.ticket_data['title'], submitter_email_id=message_id) + self.assertEqual(ticket.ticket_for_url, "q1-%s" % ticket.id) - self.assertEqual(email_count, len(mail.outbox)) - self.assertEqual(ticket.submitter_email_id, message_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): From d091a16002c07cbc694c4df3295a2807175f94e3 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Mon, 15 Feb 2016 16:16:28 -0200 Subject: [PATCH 14/30] UPDATED: when creating new Ticket instances from email messages, also save their "Message-Id" field. --- helpdesk/management/commands/get_email.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index e6a3b124..7233a3f8 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -170,8 +170,10 @@ def decode_mail_headers(string): def ticket_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() @@ -258,11 +260,12 @@ 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: + if ticket is None: t = Ticket( title=subject, queue=queue, submitter_email=sender_email, + submitter_email_id=message.get('message-id'), created=now, description=body, priority=priority, From b4c6c169c25a6c67e40e6d21a600ca019115de3c Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Tue, 16 Feb 2016 15:46:49 -0200 Subject: [PATCH 15/30] ADDED: Possibility to accept extra headers when sending emails --- helpdesk/lib.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 97977f24..3f7afd39 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -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 @@ -82,7 +82,7 @@ def send_templated_mail(template_name, email_context, recipients, sender=None, b t = EmailTemplate.objects.get(template_name__iexact=template_name, locale__isnull=True) except EmailTemplate.DoesNotExist: logger.warning('template "%s" does not exist, no mail sent' % - template_name) + template_name) return # just ignore if template doesn't exist if not sender: @@ -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: From 5f738a32143f98a99ec1b5b017d7bac5f489f91b Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Tue, 16 Feb 2016 15:49:40 -0200 Subject: [PATCH 16/30] Too much in one commit ! Splitting responsibilities when parsing email messages so we can decide when to create a Ticket and when to create a FollowUp. --- helpdesk/management/commands/get_email.py | 272 ++++++++++++---------- 1 file changed, 151 insertions(+), 121 deletions(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 7233a3f8..d442b17b 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -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,146 @@ 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_object_from_email_message(message, ticket_id, payload, files, quiet): + + ticket, new = 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') + + query_set = Ticket.objects.filter(Q(id=ticket_id)|Q(submitter_email_id=message_id)) + if query_set.count() == 0: + ticket = None + new = True + else: + t = query_set.first() + + # New issue, create a new instance + if ticket is None: + t = Ticket.objects.create( + title=payload['subject'], + queue=queue, + submitter_email=sender_email, + submitter_email_id=message_id, + created=now, + description=payload['body'], + priority=payload['priority'], + ) + t.save() + + new = True + update = '' + + # Old issue being re-openned + 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 = now, + public = True, + comment = payload['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 + + + 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, + 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: + 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 + +def object_from_message(message, queue, quiet): # 'message' must be an RFC822 formatted message. msg = message + + #import ipdb;ipdb.set_trace() message = email.message_from_string(msg) subject = message.get('subject', _('Created from e-mail')) @@ -196,9 +332,9 @@ def ticket_from_message(message, queue, quiet): matchobj = re.match(r".*\["+queue.slug+"-(?P\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 = [] @@ -241,14 +377,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 @@ -260,116 +389,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 is None: - t = Ticket( - title=subject, - queue=queue, - submitter_email=sender_email, - submitter_email_id=message.get('message-id'), - 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() From c2e9ee26af0e3d9124e3771ac9bbb429b21c12bc Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Tue, 16 Feb 2016 16:11:10 -0200 Subject: [PATCH 17/30] UPDATED: Stop storing the Message-Id field on the model and move it to the so we can easily follow the threads. --- helpdesk/models.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/helpdesk/models.py b/helpdesk/models.py index d18614ed..009ad88f 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -370,15 +370,6 @@ class Ticket(models.Model): 'follow-ups left for this task.'), ) - submitter_email_id = models.CharField( - _('Submitter E-Mail ID'), - max_length=256, - blank=True, - null=True, - help_text=_("The Message ID of the submitter's email."), - editable=False, - ) - assigned_to = models.ForeignKey( settings.AUTH_USER_MODEL, related_name='assigned_to', @@ -675,6 +666,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: From be07fdff6cd6f7024b87d48a418b9ee61d61281a Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Tue, 16 Feb 2016 17:10:13 -0200 Subject: [PATCH 18/30] UPDATED: Finished moving Message-Id field from to model. --- helpdesk/management/commands/get_email.py | 51 ++++++++--- ...re_message_id_field_into_followup_model.py | 24 ++++++ helpdesk/models.py | 27 +----- helpdesk/tests/test_ticket_submission.py | 86 ++++++++++--------- helpdesk/views/staff.py | 1 + 5 files changed, 111 insertions(+), 78 deletions(-) create mode 100644 helpdesk/migrations/0013_store_message_id_field_into_followup_model.py diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index d442b17b..3fb48516 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -168,9 +168,25 @@ def decode_mail_headers(string): decoded = decode_header(string) return u' '.join([unicode(msg, charset or 'utf-8') for msg, charset in decoded]) +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 + + 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) + def create_object_from_email_message(message, ticket_id, payload, files, quiet): - ticket, new = None, False + ticket, followup, new = None, None, False now = timezone.now() queue = payload['queue'] @@ -178,24 +194,28 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): 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: + followup = FollowUp.objects.get(message_id=in_reply_to) + ticket = followup.ticket - query_set = Ticket.objects.filter(Q(id=ticket_id)|Q(submitter_email_id=message_id)) - if query_set.count() == 0: - ticket = None - new = True else: - t = query_set.first() + try: + t = Ticket.objects.get(id=ticket_id) + new = False + except Ticket.DoesNotExist: + ticket = None # New issue, create a new instance if ticket is None: t = Ticket.objects.create( - title=payload['subject'], - queue=queue, - submitter_email=sender_email, - submitter_email_id=message_id, - created=now, - description=payload['body'], - priority=payload['priority'], + title = payload['subject'], + queue = queue, + submitter_email = sender_email, + created = now, + description = payload['body'], + priority = payload['priority'], ) t.save() @@ -213,6 +233,7 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): date = now, public = True, comment = payload['body'], + message_id = message_id, ) if t.status == Ticket.REOPENED_STATUS: @@ -242,6 +263,9 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): context = safe_template_context(t) + if cc_list is not None: + create_ticket_cc(t, cc_list.split(',')) + if new: if sender_email: @@ -302,6 +326,7 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): return t + def object_from_message(message, queue, quiet): # 'message' must be an RFC822 formatted message. diff --git a/helpdesk/migrations/0013_store_message_id_field_into_followup_model.py b/helpdesk/migrations/0013_store_message_id_field_into_followup_model.py new file mode 100644 index 00000000..2c1106c7 --- /dev/null +++ b/helpdesk/migrations/0013_store_message_id_field_into_followup_model.py @@ -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'), + ), + ] diff --git a/helpdesk/models.py b/helpdesk/models.py index 009ad88f..12bb8b74 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -558,29 +558,7 @@ class Ticket(models.Model): def get_absolute_url(self): return ('helpdesk_view', (self.id,)) - get_absolute_url = models.permalink(get_absolute_url) - - def process_rfc_2822_data(self): - - if len(self.rfc_2822_items) > 0: - cc_list = self.rfc_2822_items.get('rfc_2822_cc', []) - - for cced_email in cc_list: - - user = None - - user_model = get_user_model() - - try: - user = user_model.objects.get(email=cced_email) - except user_model.DoesNotExist: - pass - - # Local import to deal with non-defined / circular reference problem - from .views import staff - - ticket_cc = staff.subscribe_to_ticket_updates(ticket=self, user=user, email=cced_email) - + get_absolute_url = models.permalink(get_absolute_url) def save(self, *args, **kwargs): if not self.id: @@ -594,9 +572,6 @@ class Ticket(models.Model): super(Ticket, self).save(*args, **kwargs) - # Process RFC 2822 fields, if any - self.process_rfc_2822_data() - class FollowUpManager(models.Manager): def private_followups(self): diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index e33b3f89..cc4c9ad5 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -2,14 +2,14 @@ import email import uuid -from helpdesk.models import Queue, CustomField, Ticket, TicketCC +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.test.client import Client from django.core.urlresolvers import reverse -from helpdesk.management.commands.get_email import ticket_from_message +from helpdesk.management.commands.get_email import object_from_message try: # python 3 from urllib.parse import urlparse @@ -57,7 +57,7 @@ class TicketBasicsTestCase(TestCase): submitter_email = 'foo@bar.py' msg.__setitem__('Message-ID', message_id) - msg.__setitem__('subject', self.ticket_data['title']) + 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;') @@ -65,12 +65,15 @@ class TicketBasicsTestCase(TestCase): email_count = len(mail.outbox) - ticket_from_message(str(msg), self.queue_public, quiet=True) - ticket = Ticket.objects.get(title=self.ticket_data['title'], submitter_email_id=message_id) + 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) + # 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)) @@ -81,29 +84,35 @@ class TicketBasicsTestCase(TestCase): "rfc_2822_cc" field when creating a instance. """ + msg = email.message.Message() + message_id = uuid.uuid4().hex + submitter_email = 'foo@bar.py' + cc_list = ['bravo@example.net', 'charlie@foobar.com'] - email_data = { - 'Message-ID': message_id, - 'cc': ['bravo@example.net', 'charlie@foobar.com'], - } - - # Regular ticket from email creation process - self.ticket_data = { - 'title': 'Test Ticket', - 'description': 'Some Test Ticket', - 'rfc_2822_cc': email_data.get('cc', []) - } + 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) - ticket_data = dict(queue=self.queue_public, **self.ticket_data) - ticket = Ticket.objects.create(**ticket_data) + + 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) - self.assertEqual(email_count, len(mail.outbox)) + + # 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)) + # Ensure that is created - for cc_email in email_data.get('cc', []): - + 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) @@ -115,29 +124,28 @@ class TicketBasicsTestCase(TestCase): "rfc_2822_cc" field is provided when creating a instance. """ + msg = email.message.Message() + message_id = uuid.uuid4().hex + submitter_email = 'foo@bar.py' + cc_list = ['null@example', 'invalid@foobar'] - email_data = { - 'Message-ID': message_id, - 'cc': ['null@example', 'invalid@foobar'], - } - - # Regular ticket from email creation process - self.ticket_data = { - 'title': 'Test Ticket', - 'description': 'Some Test Ticket', - 'rfc_2822_cc': email_data.get('cc', []) - } + 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) - 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)) - # Ensure that is created - for cc_email in email_data.get('cc', []): + object_from_message(str(msg), self.queue_public, quiet=True) + ticket = Ticket.objects.get(title=self.ticket_data['title']) + + # Ensure that is created but the email field is not set + for cc_email in cc_list: self.assertEquals(0, TicketCC.objects.filter(ticket=ticket, email=cc_email).count()) def test_create_ticket_public(self): diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 3aaac599..50186293 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -346,6 +346,7 @@ def subscribe_to_ticket_updates(ticket, user=None, email=''): except ValidationError: email = '' + ticketcc = TicketCC() ticketcc.ticket = ticket ticketcc.user = user From fc02aa7cbb7f516ce5c723f4085c01ac1bf4fc1a Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Tue, 16 Feb 2016 17:17:58 -0200 Subject: [PATCH 19/30] UPDATED: Better exception handling + aestetics --- helpdesk/management/commands/get_email.py | 35 ++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 3fb48516..70544145 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -186,7 +186,7 @@ def create_ticket_cc(ticket, cc_list): def create_object_from_email_message(message, ticket_id, payload, files, quiet): - ticket, followup, new = None, None, False + ticket, previous_followup, new = None, None, False now = timezone.now() queue = payload['queue'] @@ -197,19 +197,22 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): cc_list = message.get('Cc') if in_reply_to is not None: - followup = FollowUp.objects.get(message_id=in_reply_to) - ticket = followup.ticket - - else: try: - t = Ticket.objects.get(id=ticket_id) + previous_followup = FollowUp.objects.get(message_id=in_reply_to) + 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 instance if ticket is None: - t = Ticket.objects.create( + ticket = Ticket.objects.create( title = payload['subject'], queue = queue, submitter_email = sender_email, @@ -217,18 +220,18 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): description = payload['body'], priority = payload['priority'], ) - t.save() + ticket.save() new = True update = '' # Old issue being re-openned - elif t.status == Ticket.CLOSED_STATUS: - t.status = Ticket.REOPENED_STATUS - t.save() + elif ticket.status == Ticket.CLOSED_STATUS: + ticket.status = Ticket.REOPENED_STATUS + ticket.save() f = FollowUp( - ticket = t, + ticket = ticket, title = _('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}), date = now, public = True, @@ -236,7 +239,7 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): message_id = message_id, ) - if t.status == Ticket.REOPENED_STATUS: + 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}) @@ -261,10 +264,10 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): print " - %s" % filename - context = safe_template_context(t) + context = safe_template_context(ticket) if cc_list is not None: - create_ticket_cc(t, cc_list.split(',')) + create_ticket_cc(ticket, cc_list.split(',')) if new: @@ -324,7 +327,7 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): fail_silently=True, ) - return t + return ticket def object_from_message(message, queue, quiet): From 57f58c34b7b67986d0321ff2b058943fba460fab Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Tue, 16 Feb 2016 17:21:46 -0200 Subject: [PATCH 20/30] UPDATED: Aesthetics --- helpdesk/management/commands/get_email.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 70544145..65127e6d 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -246,7 +246,7 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): f.save() if not quiet: - print (" [%s-%s] %s" % (t.queue.slug, t.id, t.title,)).encode('ascii', 'replace') + print (" [%s-%s] %s" % (ticket.queue.slug, ticket.id, ticket.title,)).encode('ascii', 'replace') for file in files: if file['content']: @@ -304,16 +304,16 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): else: context.update(comment=f.comment) - if t.status == Ticket.REOPENED_STATUS: + if ticket.status == Ticket.REOPENED_STATUS: update = _(' (Reopened)') else: update = _(' (Updated)') - if t.assigned_to: + if ticket.assigned_to: send_templated_mail( 'updated_owner', context, - recipients=t.assigned_to.email, + recipients=ticket.assigned_to.email, sender=queue.from_address, fail_silently=True, ) From 99bfc340f9c97d8266722dde045517f62180a39a Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Wed, 17 Feb 2016 21:43:33 -0200 Subject: [PATCH 21/30] ADDED: --- helpdesk/tests/test_ticket_submission.py | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index cc4c9ad5..899b391f 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -76,6 +76,35 @@ class TicketBasicsTestCase(TestCase): # 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 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 + 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): From e4337cef1d258db1eff216bf7314cf1612224d10 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Fri, 19 Feb 2016 11:00:35 -0200 Subject: [PATCH 22/30] ADDED: to validate if either a valid user or a valid email is provided when saving instances --- helpdesk/forms.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/helpdesk/forms.py b/helpdesk/forms.py index b766ceb6..75367e19 100644 --- a/helpdesk/forms.py +++ b/helpdesk/forms.py @@ -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 From 380723a97239f69616244859f9bb0b3cf764cb35 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Fri, 19 Feb 2016 11:09:03 -0200 Subject: [PATCH 23/30] UPDATED: Use when subscribing to a ticket followups. --- helpdesk/views/staff.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 50186293..98c0734d 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -339,29 +339,35 @@ def return_ticketccstring_and_show_subscribe(user, ticket): return ticketcc_string, SHOW_SUBSCRIBE -def subscribe_to_ticket_updates(ticket, user=None, email=''): +def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, can_update=False): try: EmailValidator()(email) except ValidationError: - email = '' + email = None + data = { + 'ticket': ticket, + 'user': user, + 'email': email, + 'can_view': can_view, + 'can_update': can_update + } - ticketcc = TicketCC() - ticketcc.ticket = ticket - ticketcc.user = user - ticketcc.email = email - ticketcc.can_view = True - ticketcc.can_update = True - ticketcc.save() + ticket_cc_form = TicketCCForm(data) + if ticket_cc_form.is_valid(): + return ticket_cc_form.save() + else: + raise ValidationError( + _('Could not create subscribe contact to ticket updated. Errors: {}'.format(ticket_cc_form.errors)) + ) - return ticketcc 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) + 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): From f911f92892598fa229f1b2e2fc0f7598ee86d08a Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Fri, 19 Feb 2016 12:25:14 -0200 Subject: [PATCH 24/30] BUGFIX: Don't create duplicated TicketCCs when processing email messages. --- helpdesk/views/staff.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 98c0734d..35790534 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -22,7 +22,6 @@ from django.contrib.auth.decorators import login_required, user_passes_test from django.core.files.base import ContentFile from django.core.urlresolvers import reverse from django.core.exceptions import ValidationError, PermissionDenied -from django.core.validators import EmailValidator from django.core import paginator from django.db import connection from django.db.models import Q @@ -341,13 +340,7 @@ def return_ticketccstring_and_show_subscribe(user, ticket): def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, can_update=False): - try: - EmailValidator()(email) - except ValidationError: - email = None - data = { - 'ticket': ticket, 'user': user, 'email': email, 'can_view': can_view, @@ -356,7 +349,16 @@ def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, ca ticket_cc_form = TicketCCForm(data) if ticket_cc_form.is_valid(): - return ticket_cc_form.save() + + 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.save() + return ticketcc.save() else: raise ValidationError( _('Could not create subscribe contact to ticket updated. Errors: {}'.format(ticket_cc_form.errors)) From 6d890509a37cde861b7bd85bcfc46013900700db Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Fri, 19 Feb 2016 12:40:18 -0200 Subject: [PATCH 25/30] ADDED: docstrings --- helpdesk/tests/test_ticket_submission.py | 209 ++++++++++++++++++++++- 1 file changed, 205 insertions(+), 4 deletions(-) diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index 899b391f..a2a1b6bf 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -6,10 +6,11 @@ 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 +from helpdesk.management.commands.get_email import object_from_message, create_ticket_cc try: # python 3 from urllib.parse import urlparse @@ -169,13 +170,213 @@ class TicketBasicsTestCase(TestCase): 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 instances are created even if the there were + no 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) - ticket = Ticket.objects.get(title=self.ticket_data['title']) + 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']) + + email_count = len(mail.outbox) - # Ensure that is created but the email field is not set + 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 is created for cc_email in cc_list: - self.assertEquals(0, TicketCC.objects.filter(ticket=ticket, email=cc_email).count()) + # Even after 2 messages with the same cc_list, 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) + + + def test_create_followup_from_email_with_valid_message_id_with_original_cc_list_included(self): + + """ + Ensure that if a message is received with an valid In-Reply-To ID, + the expected instances are created but if there's any + 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 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) + ### 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']) + + 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 is created + for cc_email in cc_list: + # Even after 2 messages with the same cc_list, 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) + + 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 + 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 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) + ### 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 is created + for cc_email in cc_list: + # Even after 2 messages with the same cc_list, 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) + def test_create_ticket_public(self): email_count = len(mail.outbox) From 32ee4de50b4d49c63267cbf318a850a1438240eb Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Fri, 19 Feb 2016 13:01:39 -0200 Subject: [PATCH 26/30] UPDATED: Better validation before creating a duplicated TicketCC --- helpdesk/management/commands/get_email.py | 6 ++++-- helpdesk/views/staff.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 65127e6d..b5688332 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -198,8 +198,10 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): if in_reply_to is not None: try: - previous_followup = FollowUp.objects.get(message_id=in_reply_to) - ticket = previous_followup.ticket + 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 diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 35790534..742f2579 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -348,7 +348,7 @@ def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, ca } ticket_cc_form = TicketCCForm(data) - if ticket_cc_form.is_valid(): + if ticket is not None and ticket_cc_form.is_valid(): queryset = TicketCC.objects.filter(ticket=ticket, user=user, email=email) From 5ed6e8c2ce483c85965bb4874a0b83ad5f491cb2 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sat, 20 Feb 2016 12:38:06 -0200 Subject: [PATCH 27/30] BUGFIX: assert statements against email count were doing the wrong math. Also, trying to explain get easier to understand comments --- helpdesk/tests/test_ticket_submission.py | 58 +++++++++++++++++++----- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index a2a1b6bf..a63dd266 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -65,6 +65,9 @@ class TicketBasicsTestCase(TestCase): 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) @@ -106,7 +109,6 @@ class TicketBasicsTestCase(TestCase): # 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): """ @@ -136,10 +138,10 @@ class TicketBasicsTestCase(TestCase): 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)) - + # 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 is created for cc_email in cc_list: @@ -217,8 +219,6 @@ class TicketBasicsTestCase(TestCase): 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) @@ -233,6 +233,16 @@ class TicketBasicsTestCase(TestCase): 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): @@ -270,6 +280,11 @@ class TicketBasicsTestCase(TestCase): 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 @@ -287,9 +302,7 @@ class TicketBasicsTestCase(TestCase): 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) @@ -298,12 +311,23 @@ class TicketBasicsTestCase(TestCase): # Ensure that is created for cc_email in cc_list: - # Even after 2 messages with the same cc_list, MUST return only - # one object + # Even after 2 messages with the same cc_list, + # 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): """ @@ -340,6 +364,11 @@ class TicketBasicsTestCase(TestCase): 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 @@ -377,6 +406,11 @@ class TicketBasicsTestCase(TestCase): 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) From 75a555631765b1aaef9967fce21a4fc77b471b58 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sat, 20 Feb 2016 12:39:16 -0200 Subject: [PATCH 28/30] UPDATED: after processing an email message, if there's any TicketCC instance linked to the Ticket being handled, mail them all. --- helpdesk/management/commands/get_email.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index b5688332..442ffddf 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -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): @@ -184,6 +184,7 @@ def create_ticket_cc(ticket, cc_list): ticket_cc = subscribe_to_ticket_updates(ticket=ticket, user=user, email=cced_email) + def create_object_from_email_message(message, ticket_id, payload, files, quiet): ticket, previous_followup, new = None, None, False @@ -271,6 +272,18 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): if cc_list is not None: create_ticket_cc(ticket, cc_list.split(',')) + ticket_cc_list = TicketCC.objects.filter(ticket=ticket).all().values_list('email', flat=True) + + if ticket_cc_list.count() > 0 : + send_templated_mail( + 'newticket_cc', + context, + recipients=ticket_cc_list, + sender=queue.from_address, + fail_silently=True, + extra_headers={'In-Reply-To': message_id}, + ) + if new: if sender_email: From 0f346924fbbdb76f36c543a490278c9ecba40c29 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sat, 20 Feb 2016 17:35:17 -0200 Subject: [PATCH 29/30] UPDATED: Moving TicketCC notifications to the end of the process so there's no need to add / subtract recipients lists (queue cc account etc). --- helpdesk/management/commands/get_email.py | 41 ++++++++++++++++------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 442ffddf..2a47139e 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -173,6 +173,7 @@ 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 @@ -183,7 +184,9 @@ def create_ticket_cc(ticket, cc_list): 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): @@ -269,23 +272,16 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): context = safe_template_context(ticket) + new_ticket_ccs = [] if cc_list is not None: - create_ticket_cc(ticket, cc_list.split(',')) + new_ticket_ccs = create_ticket_cc(ticket, cc_list.split(',')) - ticket_cc_list = TicketCC.objects.filter(ticket=ticket).all().values_list('email', flat=True) - - if ticket_cc_list.count() > 0 : - send_templated_mail( - 'newticket_cc', - context, - recipients=ticket_cc_list, - sender=queue.from_address, - fail_silently=True, - extra_headers={'In-Reply-To': message_id}, - ) + notification_template = None if new: + notification_template = 'newticket_cc' + if sender_email: send_templated_mail( 'newticket_submitter', @@ -297,6 +293,7 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): ) if queue.new_ticket_cc: + send_templated_mail( 'newticket_cc', context, @@ -317,6 +314,9 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): ) else: + + notification_template = 'updated_cc' + context.update(comment=f.comment) if ticket.status == Ticket.REOPENED_STATUS: @@ -342,6 +342,23 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): 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 From 4e32e879a8689e963c7968d90f32795bfb70c18a Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sat, 20 Feb 2016 17:36:24 -0200 Subject: [PATCH 30/30] BUGFIX: new TicketCC instances must be returned after subscribed to Ticket updates. --- helpdesk/views/staff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 742f2579..51969dd3 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -358,7 +358,7 @@ def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, ca ticketcc = ticket_cc_form.save(commit=False) ticketcc.ticket = ticket ticketcc.save() - return ticketcc.save() + return ticketcc else: raise ValidationError( _('Could not create subscribe contact to ticket updated. Errors: {}'.format(ticket_cc_form.errors))